Back

How Browser Rendering Works and Why You Should Care

How Browser Rendering Works and Why You Should Care

Within the last decades web technologies have evolved in a very rapid way. Nowadays, with things like WebAssembly, PWA, constantly progressing browser API and other surrounding technologies, web has a great potential to become a universal platform for a wide range of tasks and applications.

With web app’s area of responsibility expanding, more attention is paid to app performance. User experience became an important thing to care about, especially when you develop an app that runs on multiple devices.

To be able to manage performance appropriately, one need to know how the rendering pipeline of the browser works and where potential bottlenecks can be found.

Page Rendering

Web page load simply starts from entering the URL in the address bar. In fact, a bunch of things happen right after that and even before HTML request is done. Some of them can influence the performance, but we’ll leave this part for another dedicated article. Now we are interested in things happening when the HTML file has been loaded:

  1. Parse HTML HTML document parsed and the DOM tree as we know it has been built.
  2. Recalculate Styles All kind of stylesheets are parsed. Render tree is built. Render tree contains DOM tree nodes that are going to be displayed. Thus, head tag is excluded as well as style and script. Elements with CSS display property set to none are also ignored. On the other hand, pseudo elements, such as :after and :before, are included. Moreover, every text line becomes a single block.
  3. Layout (also called reflow) A collection of blocks generated from the render tree. All the block dimensions, that are dependent on each other, are calculated.
  4. Paint Rasterization of the blocks and texts. Images decoding and resize if necessary also happen here.
  5. Layer Composition Composition of the visual layers that can be processed and painted independently.

All the phases can be found in the Chrome DevTools’ Performance tab. In reality some of the steps above happen in parallel, e.g. CSS and HTML parsing since HTML document contains styles and stylesheet links in it, so it makes sense to perform DOM and CSSOM processing in parallel.

Any time you want to change a view on the page using javascript (or CSS animation) this pipeline gets fired. Although it is not necessary to parse HTML file and rebuild the whole render tree (usually only part of it), a proper processing for the visual update is required. Depending on requested changes, browser skips some of the steps, but in general, the pipeline would look like this:

Image of Frame Pipeline

Problem

You might not need to know all of this when you’re just adding a couple of paragraphs, or modifying background color once every blue moon. Everything changes when it comes to a complex animation and continuous DOM updates. Normally contemporary screens have frequency 60Hz, which means that new frame ought to be rendered 60 times per second (60 FPS). Therefore, browser have 1000/60 ≈ 16ms for doing his job on every frame. If it takes more time, the FPS decreases, animation might become jerky and user gets disappointed.

In the old browsers animation was usually implemented with setTimeout (or setInterval) function, which wasn’t initially designed for this purpose. setTimeout (or setInterval) function knows nothing about the browser reflow, thus visual changes might be requested at the middle of the pipeline, cancelling the last steps of the work on a previous frame and causing recalculations. Therefore we have missing frames and possible worthless work done, which might be critical in the scale of 16 ms and leads to janky animations.

Solution

Fortunately, modern browsers have a proper method to deal with it. requestAnimationFrame is the browser API that provides a sufficient way to perform animations. The call for this function (with a callback as a first argument) asks the browser to call it right before the next styles calculation, so it won’t get interrupted in the middle of the next frame processing.

requestAnimationFrame(function() {
	changeTheDOM();
})

There is a common way to implement continuous javascript animation:

let prevAnimationTime;
function handleFrame(currentAnimationTime) {
	if (!prevAnimationTime) {
	prevAnimationTime = currentAnimationTime;
}
	const diffTime = currentAnimationTime - prevAnimationTime;
	changeDOM(diffTime);
    requestAnimationFrame(handleFrame);
}
requestAnimationFrame(handleFrame);

The first argument that is passed to the callback is the time in milliseconds passed from the navigation start (from the time origin). We can use it to know the time difference with the last frame and adapt our animation properly. We can use requestAnimationFrame return value to stop an animation with cancelAnimationFrame:

let requestID;
function handleFrame() {
	changeDOM();
    requestID = requestAnimationFrame(handleFrame);
}

function startAnimation() {
    requestID = requestAnimationFrame(handleFrame);
}

function stopAnimation() {
    cancelAnimationFrame(requestID); 
}

Forced Reflow (Layout)

However, there are underwater rocks. There is a set of methods and attributes that require browser to know a layout state, e.g. offsetWidth, getComputedStyle() and others. In other words, if you do changes to the layout and then request such a property a few lines after, browser will have to recalculate all the size relations and perform layout step twice. Which, again, on the scale of 16 ms has consequences on performance.

You can find the full list of methods and properties that trigger forced layout here.

Also I would recommend this course from Paul Lewis and Cameron Pittman which talks in details about rendering pipeline and optimizations.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.