We're live-coding on Twitch! Join us!

Introduction

We live in an age where the importance of delivering web services at optimal speed can't be overemphasized. As the payload transmitted by web applications increase, developers must adopt best practices to ensure that data packets are delivered almost instantaneously, hence providing users with an overall exemplary experience.

Some of the widely adopted best practices in web development today are image compression, code minification, code bundling (with tools such as Webpack) etc. These practices already have the effect of improving user satisfaction, but it is possible to achieve more when the developer understands the underlying steps that guide the rendering of web applications to the DOM.

When playing a GPU intensive game on a low-end computing device, we may experience some juddering — shaking or vibrating — of the game characters and environment. This behaviour (though not as obvious) is also possible in web applications; users may notice when the application stops for a second or two and delays in responding to an interactive activity such as a click or scroll.

In this article, we will discuss the conditions that can enable (and prevent) a web application to run (optimally) at 60 frames per second.

What goes into a frame?

Whenever there’s a visual change in a web application, what happens under the hood is: the browser puts up a new frame for the user to see and interact with. The rate at which these frames appear (and are updated) is measured in frames per second (fps). If the browser takes too long to create and render a frame, the fps drops and the user may notice the juddering of the application.

In order to create web applications with high performance that run at 60 frames per second, the developer needs to understand the contents of a frame. Here’s a breakdown (in 5 steps) of how a frame is created:

  1. The browser makes a GET request to a remote server.
  2. The server responds with some HTML and CSS.
  3. The HTML is parsed into the DOM.
  4. The CSS is parsed into a CSS object model (CSSOM) and integrated with the DOM to create a new tree called the Render tree.
  5. The Render tree essentially consists of the elements that are displayed on the page and make up a frame.

Note: step 4 is shown in Chrome dev tools as Recalculate Styles

App Lifecycle

Before we go on to explore the browser’s rendering path and the optimizations that can be plugged into it, we need to learn about the app lifecycle as it would enable us to make smart choices in determining when an application should do the “heavy work,” hence creating smooth user experience and augmenting user satisfaction.

The app lifecycle is split into four stages:

  1. Load
  2. Idle
  3. Animation
  4. Response

Load

Before a user can interact with a web application, it has to be loaded first. This is the first stage in the app lifecycle and it is important to aim at reducing (ideally at 1s) the load time to the smallest number possible.

Idle

After an application is loaded, it usually becomes idle; waiting on the user to interact with it. The idle block is usually around 50ms long and provides the developer with the opportunity to do the heavy lifting, such as the loading the assets (images, videos, comments section) that a user might access later.

ProTip: A hack to significantly reduce load time is to load only the basics of the UI first and pull in other elements at the Idle stage.

Animate

When the user starts interacting with the application and the Idle stage is over, the application has to react properly to user interaction (and input) without any visible delay.

Note: Studies have shown that it takes about a tenth of a second (after interacting with a UI element) to notice any lag. Therefore responding to user input within this time range is perfect.

A challenge might be posed when the response to user interaction involves animation of some sort. In order to render animations that execute at 60 frames per second, each frame would have a limit of 16ms — this is basically a second divided by 60.

In reality, this should be about 10ms - 12ms due to the browser overhead. A way of achieving this would be to perform all animation calculations upfront (during the 100ms after a UI element has been interacted with).

The Browser’s Rendering Path and Various Optimizations

The Browser's rendering path takes the following route:

  • JavaScript

  • Style calculation

  • Layout creation

  • Painting of screen pixels

  • Layer composition

On a web page, when a visual change is made (either by CSS or JavaScript), the browser recalculates the styles of the affected elements. If there are changes to an element’s geometry, the browser checks the other elements, creates a new layout, repaints the affected elements and re-composites these elements together.

However, changing certain properties of a page’s elements could change the rendering path of a web page. For instance, If a paint-only property, such as background image or text colour, is changed, the layout is not affected because no changes were made to the element’s geometry. Other property changes could leave layout generation and paint out of the rendering pipeline.

We will expore some optimizations that can be plugged into the browser’s rendering path next.

JavaScript

JavaScript allows developers to provide users with awesome animations and visual experiences and is therefore heavily used in web applications. From our discussion on the app lifecycle, we see that the browser has about 10ms - 12ms to render each frame. To ease the burden of JavaScript on the rendering pipeline, it is important to execute all JavaScript code as early as possible in every frame since it could trigger other areas of the rendering pipeline.

It is possible to achieve this using the window.requesAnimationFrame() method, according to the MDN web docs:

“The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.”

The requestAnimationFrame() API enables the browser to bring in JavaScript at the right time and prevent itself from missing a frame. Here’s an example of the method in use:

function doAnimation() {
    // Some code wizardry
    requestAnimationFrame(doAnimation); //schedule the next frame

}

requestAnimationFrame(doAnimation);

The performance tab in the Chrome dev tools allows developers to record a page while in use and displays an interface that shows how JavaScript performs in the web application.

While requestAnimationFrame is a very important tool, some JavaScript code could be really resource intensive. Websites run on the main thread of our operating systems hence these scripts could stall the execution of other stages of the rendering pipeline. To solve this problem, we can use web workers.

Web workers allow us to spawn new threads for resource-intensive JavaScript code according to the MDN web docs:

“Web Workers is a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. Once created, a worker can send messages to the JavaScript code that created it by posting messages to an event handler specified by that code (and vice versa).”

To use this feature, you’ll need to create a separate JavaScript file which your main app will spawn into a web worker.

Style Calculation

Style changes are a key part of any web application’s rendering pipeline as the number of style changes required by its elements is directly proportional to the performance cost of style recalculation. Check out Paul’s website for a pretty decent breakdown of how CSS styles affect the rendering pipeline and therefore performance.

In addition to the number of style changes, selector matching should be factored into our list of rendering optimizations. Selector matching refers to the process of determining which styles should be applied to any given DOM element(s).

Certain styles may take more time to be processed than others and this becomes important as the number of elements affected by one or more style changes increases. A suitable approach to solve this issue is the Block Element Modifier (BEM) methodology. It provides great benefits for performance as class matching, which follows the BEM methodology, is the fastest selector to match for modern browsers.

Layout Creation

A major performance bottleneck is layout thrashing. This occurs when requests for geometric values interleaved with style changes are made in JavaScript and causes the browser to reflow the layout. This, when done severally in quick successions, leads to a forced synchronous layout. In this article, Googler, Paul Lewis, highlights the various optimizations that can be done to prevent forced synchronous layouts.

Painting of Screen Pixels

Painting occurs when the browser starts filling in screen pixels. This involves drawing out all visual elements on the screen. This is done on multiple surfaces, called layers. Paint could cause performance problems when it is required by large portions of the page, especially at intervals, as seen during page scrolling.

The paint profiler, as shown above, makes it easy to identify what areas of the page are being painted and when they’re being painted. The paint profiler can be found by pressing the escape key after navigating to Chrome dev tools and selecting the Rendering tab.

The first checkbox, Paint flashing, highlights in green what areas of the page are currently painted and the frequency of this could tell us if painting contributes to the performance issues on the rendering pipeline.

Looking at the image above, we can see that only the scroll bar is painted as the page is scrolled, which indicates great browser paint optimizations on the homepage of the Scotch.io website 😄.

Layer Composition

This is the final path in the browser rendering pipeline and it involves an important browser structure — Layers. The browser engine does some layer management by first considering the styles and elements and how they are ordered, then tries to figure out what layers are needed for the page and updates the layer tree accordingly.

Next, the browser composites these layers and displays them on the screen. Performance bottlenecks due to painting occur when the browser has to paint page elements that overlap one another and also exist in the same layer as one another.

To solve this problem, the elements involved will have to exist in separate layers. This can be achieved with the will-change CSS property and setting its attribute to transform :

<element_to_promote> {
    will-change: transform;
}

It should, however, be noted that an increase in layers would mean an increase in the time spent on layer management and compositing. With Chrome dev tools, it is possible to see all the layers on a page as shown below:

To get to the Layers tab, click on the hamburger menu button (three vertical dots) in Chrome dev tools, navigate to more tools, and select Layers.

Conclusion

We have taken a brief tour of the browser’s rendering pipeline, the app lifecycle and the various optimizations that can be plugged into the rendering pipeline during the animate lifecycle of a web application.

When these optimizations are implemented logically, the result is a great user experience and some inner satisfaction for the frontend developer. If reading this article has piqued your interest, you can learn more about browser rendering optimizations from this course.

There is a sequel to this article that focuses on using Chrome Dev Tools to find Performance bottlenecks. You can read about it here.

Like this article? Follow @JordanIrabor on Twitter