Browser Performance

Browser Performance

Welcome to the Browser section of my web performance series. In this segment, I'll be focusing on performance topics related to the Browser. For topics related to Javascript in general please refer to the Javascript part of the series. For bundle size and lazy loading, please check out the Network part. And If you're interested in the inner workings of the V8 engine and how to optimize your code to get the most out of it, head to the Engine part.

Rendering Path

When a Page is received by a browser it goes through these steps in a sequence:

  1. DOM: HTML is received from the server and parsed into the Document Object Model.

  2. CSSOM: CSS is received from the server and parsed into the CSS Object Model.

  3. Render Tree: The DOM and CSSOM are combined to create the Render Tree, which represents the visual elements of the page.

  4. Reflow (Layout): The Render Tree is laid out, determining the position and size of each element.

  5. Paint: The Render Tree is painted, drawing each element on the screen.

  6. Javascript processing. (more on this in Engine Part of the series)

There are multiple important things to notice in this process:

  • Steps 2 and 6 are optional if the HTML document has no reference to CSS or JS files then parsing of them won't happen.

  • Steps 1, 2, 3, and 6 usually happen only once DOM, CSSOM, and JavaScript aren't recompiled multiple times.

  • Steps 4 and 5 Reflow and Paint is what happens when we manipulate DOM/CSSOM.

  • Reflow is more resource-intensive than Paint.

  • Reflow always triggers Paint.

  • Reflow is blocking, everything waits for Reflow to finish.

  • Paint isn't blocking, it waits for the main thread to be free to begin.

  • Reflow happens when we affect the geometry of the page or if try to access its newest values.

  • Reflow of the element causes reflow of parents and children because their geometry can be affected by elements' geometry changes.

CSS

When the browser receives CSS from the server it starts the process of parsing into the CSS Object Model. This process is not that resource intensive but here are some things to take into account:

Minimizing Selector Complexity: The main thing that takes into account when thinking about optimizing CSSOM creation is element matching. nth-child(4n+1) is complicated to figure out which elements are affected by this style, and using classes to identify elements is way faster. Also, fewer selectors per element have less complexity, so faster identifications of the target element. Calculating also include which class wins, reducing the number of class that affect the element, and fewer conflicts per element faster the calculations.

Media Queries: The most effective CSS optimization technique is splitting CSS using media queries. What this means is your mobile version of the page and desktop version probably share some common styles, but also have a bunch of individual styles, especially if your mobile app has less functionality than your desktop then you will be sending a bunch of CSS code that will never be used on a mobile. This affects CSS processing speed because the browser needs to go thru all code to figure out what is and what isn't used, and secondary it affects loading speed because you are sending a bunch of CSS code that will never be used and probably is sent by a mobile network. Also if you have a printable version of a page that is usually just a blank monochrome page with a logo then you are sending a bunch of CSS that slows down printable version generation. What you can do is split your CSS into two files, and depending on the client send a mobile or desktop version. you can achieve this either on your backend server or using media queries in link tags.

  <link rel="stylesheet" href="desktop.css" media="screen and (min-width: 481px)" />
  <link rel="stylesheet" href="mobile.css" media="screen and (max-width: 480px)" />
  <link rel="stylesheet" href="print.css" media="print" />

Font-Display Fallback: The last thing to mention is, font-display: fallback; the property provides a fallback font while the actual font is being loaded, ensuring that the user sees text immediately rather than waiting for the font to load. This can greatly improve the user experience and prevent the dreaded "flash of invisible text" (FOIT).

Reflow & Paint

When we use Javascript to manipulate DOM most of the time it will result in Reflow which will trigger Paint. However, as mentioned in the bullet point, Reflow happens on geometry change or its read. So if we change elements without affecting any geometry changes only the Paint process will take place, for example changing opacity won't result in Reflow but will in Paint, also transform because It works by applying a transformation matrix to the element, which changes the position, size, or shape of the element without affecting the layout or position of other elements on the page.

There are multiple ways to avoid or reduce the number of Reflow:

  • Reducing DOM manipulations, fewer manipulations fewer issues.

  • Batching them since the browser will optimize multiple DOM changes.

  • Change only the lowest level DOM tree elements, this will affect less amount of elements and a list amount of recalculation will need to take place.

  • Avoid inline style modification, inline styles are part of the DOM changes to it will result in Reflow. it is especially bad if those changes aren't batched, then Reflow will be triggered on each change to the style attribute. Instead, use classes to avoid redundant reflows.

  • Debounce windows resize, window resize is the worst case of Reflow because it affects all elements so everything needs to be recalculated, and you have to do this every millisecond.

  • Sapareting and reducing DOM reads from writes. What this means is, for example, element.offsetWidth will result in reflow, because the browser wants to give you the up-to-date width of an element, so it will reflow which will trigger repaint and then give you the width of the element. So if you are writing to DOM by changing the width of an element element.style.width and then reading its new width element.offsetWidth this will result in two reflows and two repaints every time. You can read about more reflow triggers here What forces layout/reflow.

  • Reduce the smoothness of animations, if your animation takes 3s and you wrote DOM manipulation that takes place every 100ms it will result in 30 Relows. Also reducing reads in the animation process if you are changing DOM then reading from DOM then Changing DOM, will have a huge impact on performance.

  • Read element dimensions and use the local Javascript state to store dimensions, and update it parallel to DOM manipulation to keep track and avoid using Read that triggers Reflows.

Javascript

Here are Javascript browser-specific optimization technics. For topics related to Javascript in general refer to the Javascript part of the series.

CSS Transitions/Animations can be used to create animations and other effects in a more efficient way than using JavaScript. By offloading these operations to the browser, you can free up resources for other parts of your application.

RequestAnimationFrame is a method that gives you the ability to run an animation function on every screen refresh, 60 calls per second for most screens, without sacrificing performance and making sure that animations speed is consistent on monitors of different refresh rates. It also has built-in optimization that pauses those animations when a tab is in the background or animation is inside or a hidden <iframe>.

const element = document.getElementById("box");
let moveBy = 0;
function animation(currentTimestamp) {
  moveBy += 10;
  element.style.transform = `translateX(${moveBy}px)`;
  if (moveBy >= 1000) return;
  window.requestAnimationFrame(animation); 
}
window.requestAnimationFrame(animation);

Event Listeners Delegation is a technique used to reduce the number of event listeners attached to elements. Instead of attaching an event listener to each individual element, event listeners are attached to a parent element. By attaching an event listener to a parent element, you can capture events that occur on any of its child elements. This eliminates the need to attach event listeners to each child element individually. Note that not all events can be delegated e.g. focus and load. In addition event listeners delegation also has the advantage of making it easier to dynamically add or remove elements to the page. Since the event listener is attached to the parent element, any new child elements that are added to the parent element will automatically be included in the event delegation.

/*  <ul id="item-list">
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul> */
const itemList = document.querySelector('#item-list');
itemList.addEventListener('click', (event) => {
  if (event.target.tagName === 'LI') {
    console.log(event.target.textContent);
  }
});

Web Workers are useful for offloading expensive or long-running tasks to a separate thread. This allows the main thread of the application to remain responsive and handle user interaction. Examples of this can be complex data processing operations or image, audio or video processing.

Service Workers are a type of web worker that focuses on enabling advanced caching and offline functionality in web applications. It runs on a background thread and can intercept network requests made by the web application. This allows them to cache resources such as HTML, CSS, and JavaScript files, as well as images, fonts, and other assets. When a user revisits the web application, the service worker can use the cached resources to load the application more quickly, even if the user is offline. Moreover, you can configure it to also cache POST requests and short-circuit repetitive or redundant requests to an API.

Compositor Thread

Browsers have many threads a main thread, an IO thread, special-purpose threads, and general-purpose threads. However, we have access to only main and worker threads. The main thread is placed when our Javascript runs and Rendering happens and the Worker thread is when we use our web workers. But there is one more important thread that we don't have direct access to Compositor thread aka GPU Thread.

The compositor thread is responsible for actually displaying pixels on a screen, it takes bitmaps that are created on the Main thread in process of Painting and draws them on the screen using GPU. We don't have direct access to this thread but it is important because GPU is good at drawing the same bitmap over and over, in different places, scaling and rotating it, making them transparent, applying filters, etc.

So if we have an element on a page like a slide bar that just slides from left to right there is no reason for the main thread to do a bunch of calculations on a page every time we trigger sliding. Instead, it would be great to tell the browser not to recalculate anything and just make a GPU thread to reuse the slide bar bitmap and move it back and forth.

Luckily browser already has such logic it looks at your page and brakes it down into separate Layers so if needed it can tell GPU to manipulate these layers individually without the need for a bunch of unnecessary calculations on the main thread. This breakdown into threads is Indeterministic for us, it is handled by the browser so it decides if a page will be one layer or multiple, also it is dynamic, the browser starts with one layer then go to multiple then merge multiple layers into one and so.

The only thing that we can suggest to a browser that something for example sidebar is better to be placed on a separate Layer. We can do it using CSS property will-change and specifying properties that will change e.g. transform property. Now the browser will know that this element can be its layer and its transformation will happen on the GPU thread instead of the main thread.

A good practice is to add this only when needed e.g. .sidebar:hover { will-change: transform; } so that we will add this property only when the user hovers over the sidebar before a user clicks a button, and transform will happen and remove when the user leaves the sidebar.

An even better practice is not to use this at all (MDN)