Browser Rendering Queue in-depth.

An in-depth full explanation on how browsers manage to push pixels on the screen, and how to improve performance.

Cinema picture
Feel free to take a seat!

Before you read

This is the continuation of this previous article on browsers' internals (JavaScript focused), if you haven't already read it, take 10-minutes and give it a shot (good content inside, I promise emoji 😉).

The critical importance of delivering 60FPs

If you are just doing simple stuff like CSS animations, simple landing pages, using few and lightweight elements, avoiding fancy / esoteric / satanic things, you may not be interested in this article, you can just code and everything should be fine also in mobile devices.

Instead, if your are dealing with more complex apps, with many time-sensitive and complex animations, with thousands of different and/or heavy elements, being performant and hitting 60FPs (Frame per second) could be hard, this article should be useful in order to have a better understanding on what’s happening, and how to create a better, optimized and performant code.

Why 60FPs? Because with that frame rate the human eye sees the image flow as continuous. Here a practical comparison between 30 and 60 FPS:

30 vs 60 FPs comparison

In other terms this means that to provide the best user experience to your customers the browser must be able to generate at least 60 frames per second, which is 1 frame every 16ms10ms (the browser has to do also other stuff, so you won't have 16ms all for you).

Rendering what?

The Rendering UI Queue is the process by which browsers manage to transform your abstract code (HTML + CSS + JavaScript) into raster images to show to the end user. This process is sometimes referred also as rendering path or critical rendering path.

This process consists of a long pipeline of different operations needed to interpret the code, respond to animations and user interactions (events) (ex. click, focus, type, etc.), keep everything consistent, create the final raster(s) for the given frame and sync with the GPU (send the so-constructed raster to the GPU, which will take care of displaying it on the screen).

Understanding this process going on behind the scenes is a key part in order to proper analyze, debug and engineer how to structure a complex Front-end application, deal with performance bottlenecks and unexpected frame rate drops, create a fast, performant and smooth user interfaces and experiences.

Rendering Queue steps list picture
Rendering Queue full steps list
Rendering Queue picture
Rendering Queue

emoji ⚙ Let's see the single operations in detail

  • DOM parsing: parse the HTML code into a machine-understandable tree, called DOM (Document Object Model);
  • CSS parsing: parse the CSS code into another machine-understandable tree, emoji 🌲 it seems that computers like trees, I like them as well emoji 🌲;
  • CSSOM creation: this is a merge between the previously parsed DOM and CSS, called CSSOM (CSS Object Model). It contains the information of all the DOM elements, along with their styles (critical for the next step, keep reading);
  • Render tree creation: this is kind of a cleaned CSSOM, in fact it contains only the DOM elements that will be rendered in the page: if for example some DOMNode is styled with display: none; in the CSSOM, it won't be in this tree as it won't be rendered (displayed) on the screen;
  • Layouting: sometimes also referred as Reflow, this is the process of calculating the sizing and positions of every single element from the Render tree;
  • Layer tree creation: it is not granted that there will be only one final raster of the page. The browser (or you using some imperative styling rule ex. will-change: transform;) can decide that for the given page it is better to split it into different layers (rasters). This is usually done for performance reasons: if a raster has to be changed multiple (many) times while the others must remain unchanged this can avoid un-needed repaints of un-changed items. This process figures out how many layers need to be created and which elements will fall inside each one;
  • Painting: at this stage we know everything of our code, everything has been reordered into digestible trees and we are ready to actually paint the pixels in rasters (in Chrome this process is made via the low-level open-source 2D graphic library Skia);
  • GPU sync: the fresh painted rasters from the Painting stage are sent to the GPU memory with all the necessary management information;
  • Composition: this is the final stage, the GPU positions every single raster in the right position and with the right effects applied (ex. opacity), creating the final frame on the screen.

emoji ⚖ On computational costs and performance

Every frame has to be consistent with the actual code, hence the browser watches for HTML or CSS changes to restart the Rendering pipeline and update the showed frame. The pipeline has not to be executed in his totality, in order to save precious time and gain performance, the browser executes only the strictly needed operations.

For example, if using JavaScript you manipulate the DOM, the browser has to start the pipeline from the first steps, but it you have only added an hidden element, it will skip the Layouting and Painting steps as nothing as changed geometries or visible things (the old frame keeps its validity).

Usually the most time-consuming processes (possible bottlenecks) are the Layouting and Painting steps: the first increases with the number of elements, the latter increases with the size of them and their styling effects (painting a uniform colored background is much less computational expensive than dealing with shadows and gradients!).

TIP: you can use Chrome DevTools to quickly identify paint issues and areas that are constantly re-painted, go to the rendering tab in the DevTools panel and choose “Show paint rectangles”.

Show paint rectangles picture
"Show paint rectangles" DevTools option
You won't believe at what you'll see...

Another possible bottleneck for un-experienced JavaScript developers is to inadvertently force a re-layouting or reflow via JavaScript. This happens if you first invalidate the current frame (for example changing the height of an element), then you read the measures (for example reading the width of another element).

This way you are trying to read a value from a current invalidated frame, hence the browser has to build a new frame in order the let you read the right (updated) values.

Doing a loop of write/read styles could be a very big error, the best thing is to group all the write/read requests and batch them as a single operation. This issue is sometimes referred also as Layout Trashingemoji 🚮.

//The wrong way:
for ( let i = 0; i < 10; i++ ) {
	changeHeight();
	readWidth();
}

//The right way:
//Using two loops is way more performant than forcing reflows!
for ( let i = 0; i < 10; i++ ) {
	//Do all the changes first.
	changeHeight();
}
for ( let i = 0; i < 10; i++ ) {
	//Read all them after.
	readWidth();
}

Using composition-phase-only properties emoji 🐎

These properties are the best to be used and manipulated for animations and interactions, in fact they trigger only the composition phase of the Rendering pipeline. This way they assure to be super-fast and deliver 60+Frame/s using GPU-accelerated operations only.

Following a list of the most common CSS properties and their invasiveness in the rendering process:

CSS PropertyInvasiveness
opacitycomposite
cursorcomposite
z-indexcomposite
user-selectcomposite
transformcomposite
colorpaint
border-stylepaint
visibilitypaint
backgroundpaint
text-decorationpaint
background-imagepaint
background-positionpaint
background-repeatpaint
outline-colorpaint
outlinepaint
outline-stylepaint
border-radiuspaint
border-radiuspaint
box-shadowpaint
outline-widthpaint
box-shadowpaint
background-sizepaint
CSS PropertyInvasiveness
widthlayout
heightlayout
paddinglayout
marginlayout
displaylayout
border-widthlayout
borderlayout
toplayout
positionlayout
font-sizelayout
floatlayout
background-colorlayout
border-colorlayout
text-alignlayout
overflow-ylayout
font-weightlayout
overflowlayout
leftlayout
font-familylayout
margin-bottomlayout
padding-bottomlayout
line-heightlayout
vertical-alignlayout
rightlayout
clearlayout
white-spacelayout
list-style-typelayout
bottomlayout
zoomlayout
font-stylelayout
fontlayout
min-heightlayout
max-widthlayout
min-widthlayout
list-stylelayout
contentlayout
border-collapselayout
text-shadowlayout
box-sizinglayout
text-indentlayout
border-horizontal-spacinglayout
border-spacinglayout
max-heightlayout
text-transformlayout
text-overflowlayout
word-wraplayout
letter-spacinglayout
appearancelayout
directionlayout

emoji 📚 Further readings

Avoid Large, Complex Layouts and Layout Thrashing!

Simplify Paint Complexity and Reduce Paint Areas

Rendering Queue CSS Triggers

High Performance Animations

For updates, insights or suggestions, feel free to post a comment below! emoji 🙂


Responses

Latest from Web Engineering see all

JavaScript: in depth practical explanation on closures. | cover picture
JavaScript: in depth practical explanation on closures.
Created on 21 July 2018.
A deep look at JavaScript closures with explanations and hands-on code examples.
JavaScript: differences between using var, let and const. | cover picture
JavaScript: differences between using var, let and const.
Created on 19 June 2018.
A close look and explanation at how to properly use the new ES6 JavaScript variable declaration keywords.
JavaScript: how to find a key-value pair in a nested object. | cover picture
JavaScript: how to find a key-value pair in a nested object.
Created on 17 June 2018.
How to recursively traverse a JavaScript nested and unordered object to find if a value exists at a specific key.
×