Skip to main content
menu

BMT Systems

  WEB & JAVASCRIPT  

Mobile Navigation Controls

Preventing Forced Reflows with Strategic JavaScript: I

Thomas M. Brodhead

This is part 1 of a two-part article on forced reflows.

Forced Reflow Overview

DOM reflow is an updating of the geometry of DOM elements. This will happen in JavaScript routines when inline styles or classes are changed on elements. Less obviously, it will occur when the geometries of elements are queried. It may be surprising to learn that a simple geometry query—such as fetching the width of an element—will trigger a reflow. The browser must do this to account for changes to DOM elements by JavaScript edits, as well as by changes by the user to the display (e.g., resizing the window).

When reflows take place in between browser animation frames, they may produce a noticeable jank of the screen. This jank is caused by a forced reflow, a.k.a. forced synchronous layout. To alert developers of these problems, the Chrome DevTools panel will show a warning message like this:

[Violation] Forced reflow while executing JavaScript took 129ms

The JavaScript may be audited with the Chrome DevTools to pinpoint the problematic code. But even when that code is identified, its cure may buck the traditional practice of succinct, elegant coding: short code structures do not necessarily translate into efficient performance in JavaScript when the DOM is queried or altered. Reworking coding strategies in JavaScript may require rethinking JavaScript.

Pseudo-code Example

Consider this pseudo-code JavaScript routine:

brightness_high content_paste
elementArray.forEach(function (element, index) {
    element.width = differentElement[index].width + 'px';
});

It looks reasonable: the width of one element is set relative to the width of a different element. It’s also short and to the point. But it’s not optimal. It will cause DOM thrashing and trigger a [Violation] Forced reflow message in the DevTools console.

Querying an element size and setting an element size will cause forced reflows on each iteration of the forEach loop. Every time the differentElement.width is queried, the DOM must be reconstructed to account for the previous setting of element.width and any cascading changes it may have caused. The DOM reconstuctions will take place in between animation frames, and they may cause a visible jank. Even if not noticeable, this will slow page rendering and will make the site less performant.

The cure to forced reflows is to separate measurements from mutations into batches, and to ensure that each batch executes at the beginning—not in the middle—of animation frames. JavaScript lacks an elegant way to do this, and so it must be constructed with brute force coding.

Chrome DevTools watches for this performance hiccup and reports it in the console when found. The question is how to pinpoint the offending code in the JavaScript and then how to swat it.

We’ll cover that in detail, but first we need to explore and define DOM measurements and mutations.

DOM Measurements and Mutations

Reflow-causing operations fall into two categories: (1) operations that measure elements and (2) operations that mutate elements.

To prevent forced reflow, operations in each category first must be identified and separated into dedicated groups. Below is a listing of the most common operators/functions that cause DOM reflow, each identified by its category (measure, mutate, or both, in the case of operators that perform double duty):

  • CharacterData:
  • data: Mutate
  • remove: Mutate
  •  
  • Document:
  • elementFromPoint: Measure
  • elementsFromPoint: Measure
  • execCommand: Mutate
  • scrollingElement: Measure
  •  
  • Element:
  • classList: Measure or Mutate
  • className: Mutate
  • clientHeight: Measure
  • clientLeft: Measure
  • clientWidth: Measure
  • getBoundingClientRect: Measure
  • getClientRects: Measure
  • innerHTML: Mutate
  • insertAdjacentHTML: Mutate
  • outerHTML: Mutate
  • remove: Mutate
  • removeAttribute: Mutate
  • scrollBy: Mutate
  • scrollHeight: Measure
  • scrollIntoView: Mutate
  • scrollLeft: Measure or Mutate
  • scrollTo: Mutate
  • scrollTop: Measure or Mutate
  • scrollWidth: Measure
  • setAttribute: Mutate
  •  
  • HTMLButtonElement, HTMLFieldSetElement, HTMLInputElement, or HTMLKeygenElement:
  • reportValidity: Measure
  •  
  • HTMLDialogElement:
  • showModal: Mutate
  •  
  • HTMLElement:
  • blur: Measure
  • focus: Measure
  • innerText: Measure or Mutate
  • offsetHeight: Measure
  • offsetLeft: Measure
  • offsetParent: Measure
  • offsetTop: Measure
  • offsetWidth: Measure
  • outerText: Measure or Mutate
  • style: Measure or Mutate
  •  
  • HTMLImageElement:
  • x: Measure
  • y: Measure
  •  
  • MouseEvent:
  • layerX: Measure
  • layerY: Measure
  • offsetX: Measure
  • offsetY: Measure
  •  
  • Node:
  • appendChild: Mutate
  • insertBefore: Mutate
  • removeChild: Mutate
  • textContent: Mutate
  •  
  • [Object Instance]:
  • getComputedStyle: Measure
  • scroll: Mutate
  • scrollBy: Mutate
  • scrollTo: Mutate
  •  
  • Range:
  • getBoundingClientRect: Measure
  • getClientRects: Measure
  •  
  • SVGElement:
  • currentScale: Measure or Mutate

Step 1: Separate Measurements and Mutations into Batches

Now that we’ve identified DOM reflowing operations and categorized them, we can return to the pseudo-code presented earlier and rewrite it.

First, tease the original forEach loop into two separate forEach loops, one collecting measurements, the other setting mutations:

brightness_high content_paste
// elementArray = [...];
// differentElement = [...];
widthArray = [];

// Collect measurements:
differentElement.forEach(function (element) {
    widthArray.push(element.width);
});

// Assign mutations:
elementArray.forEach(function (element, index) {
    element.width = widthArray[index] + 'px';
});

Step 2: Wrap Batches in requestAnimationFrame Handlers

The previous step doesn’t ensure that each routine begins on an animation frame. To do that, we use window.requestAnimationFrame(). It accepts a function as an argument, and that function is moved to the beginning of the next available animation frame. By placing DOM reflowing operations into a function called by window.requestAnimationFrame(), we ensure that they do not execute mid-animation frame and therefore do not cause a forced reflow.

Putting window.requestAnimationFrame() wrappers around the two forEach loops is the ticket:

brightness_high content_paste
// elementArray = [...];
// differentElement = [...];
widthArray = [];

// Collect measurements:
window.requestAnimationFrame(function () {
    differentElement.forEach(function (element) {
        widthArray.push(element.width);
    });
});

// Assign mutations:
window.requestAnimationFrame(function () {
    elementArray.forEach(function (element, index) {
        element.width = widthArray[index] + 'px';
    });
});

Fastdom: An Alternative Solution to Naked requestAnimationFrames

A more comprehensive solution is to use fastdom, a third-party JavaScript utility that batches measurements and mutations efficiently. It puts the operations into queued batches and calls requestAnimationFrame for each batch.

Fastdom distinguishes between measurement batches and mutation batches with dedicated function calls. Instead of wrapping measurements and mutations into identical window.requestAnimationFrame() functions, use window.fastdom.measure() as the wrapper for measurement operations, and use window.fastdom.mutate() as the wrapper for the mutation operations. This ensures the batches execute in the correct order.

One way to use fastdom is to nest calls to fastdom.mutate inside fastdom.measure, like this:

brightness_high content_paste
widthArray = [];

// Collect measurements:
window.fastdom.measure(function () {
    differentElement.forEach(function (element) {
        widthArray.push(element.width);
    });
    window.fastdom.mutate(function () {
        elementArray.forEach(function (element, index) {
            element.width = widthArray[index] + 'px';
        });
    });
});

Summing up

We’ve covered forced reflows, the measurement and mutation operations on the DOM that cause them, and a pseudo-JavaScript example of offending code and its solution. In Preventing Forced Reflows with Strategic JavaScript II, we’ll use a short but complete application to see reflows in action, Chrome DevTools for diagnosing them, and actual code that solves the problem.

Further Reading

Aside