What is a Forced Reflow and How to Solve it?

Force reflow (or Layout Reflow) is a major performance bottleneck. It happens when a measurement of the DOM happens after a DOM mutation. With this knowledge, I was able to improve performance of an app in my workplace by 75%. Read on to understand how.

The browser is a wondrous thing. We give it JS, HTML and CSS – and they are translated into visual wonders. It does it by running the same rendering cycle again and again. Sometimes, something in the cycle can go wrong. Layout reflow is one of those things.

I wrote about the Critical Rendering Path (CRP) in a former article. In a nutshell, the regular flow of the code in the browser is this:

Figure 1: The healthy CRP diagram

Forced Reflow is a disturbance in the force… sorry… in the flow. That means that we force a later stage (layout) into our javascript.

A reflow looks more like this:

Figure 2: The CRP diagram for a reflow

Figure 2 illustrates a reflow. The Javascript code caused the browser to initiate style and layout calculations during its run.

The calculations were done, and the Javascript continued until it finished. The rest of the flow runs then.

Besides the fact we might run costly style and layout calculations twice – our javascript now takes much longer to run.

How forced reflow is created?

When you query the DOM for size or position, the result is usually taken from former calculations.  The browser knows how the DOM looks like, and if it knows it didn’t change, it just gets the correct value from the layout cache (created in the former calculation).

Querying the DOM is called: Measure.

Now, let’s assume you are changing the DOM. Appending elements, changing height/width or position of elements etc.

Changing the DOM is called: Mutate.

After you are changing the DOM, the browser flags its layout cache as invalid and schedules a recalculation.  If you measure the size or position of an element at this stage, the browser needs to recalculate the whole DOM in order to give you the real answer.

The reflow happens when during Javascript we mutate the DOM and then measure it. For instance, in the code below, we change the height of an element and then query its height.

element.style('height', 500);
console.log(element.style('height'));

Exercise – Create a Forced Reflow

In this exercise you will see an example for Forced reflow while executing JavaScript.

  1. Clone the following repository: https://github.com/YonatanKra/performanceWorkshop
    git clone https://github.com/YonatanKra/performanceWorkshop
  2. Type: checkout layout-reflow-1
  3. Run yarn or npm i
  4. Run yarn demo or npm run demo
  5. Open the app in the browser: http://localhost:3000
  6. Query the server (just use the input field at the top)
  7. Start monitoring
  8. Click the sorting buttons
  9. Stop the recording

A short TL;DC (too long, didn’t clone) – the app queries a list of users from a server. It then allows you to sort the users by their ID or name.

Here’s the result of the sorting scenario described above:

Figure 3: Layout reflow monitoring result. Purple part under the yellow javascript part is the reflow.

You can see that the style and layout parts (the purple part) are now inside the javascript part – causing it to run longer. That’s the reflow!

Let’s compare it to the CRP recording of a reflow-free code:

Figure 4: A “healthy” CRP recording as shown in a former article

You can see that the style and layout parts start after the javascript finished running.

The reflow in Figure 3 happens because a simple line that was added to the code. In the data-table.component.js file:

refreshData(data, clear = false) {
if (!clear) {...} else {...}
data.forEach(datum => {
const element = document.createElement('data-table-row');
element.setAttribute('name', datum.name);
element.setAttribute('id', datum.id);
element.setAttribute('email', datum.email);
this._dataTable.prepend(element);
});
DataApp.emitEvent(this, 'refreshed-data', {
scrollHeight: this._dataTable.scrollHeight
});
}
Code snippet 1: The method refreshData measures the DOM in line 41 (scrollHeight) which initiates layout calculation (reflow).

Line 13 in the code snippet #1 emits an event when we finish loading the data. Inside, it measures the DOM and sends the updated scrollHeight (line 14). An innocent product demand, right?

The problem arises from the fact that line 4 starts the process of adding elements to the DOM (mutating the DOM). When the emit event function queries the DOM (line 14), the Layout Cache is invalid, and a layout calculation is initiated during our JavaScript run (and forces a reflow of the layout).

How to Solve Forced Reflow?

Solving a Forced Reflow is usually straight forward. You just need to avoid a DOM measurement after a DOM mutation in the same CRP.

One way to do it is to just switch places between the measurement and the mutation. For instance code snippet 2:

refreshData(data, clear = false) {
DataApp.emitEvent(this, 'refreshed-data', {
scrollHeight: this._dataTable.scrollHeight
});
if (!clear) {...} else {...}
data.forEach(datum => {
const element = document.createElement('data-table-row');
element.setAttribute('name', datum.name);
element.setAttribute('id', datum.id);
element.setAttribute('email', datum.email);
this._dataTable.prepend(element);
});
}
Code snippet 2: The same code from code snippet 1 without the reflow.

Code snippet 2, while solving the forced reflow, is not so useful. We are sending an obsolete scroll height measurement in our event – even before the data was set on screen.

A more robust solution would be to defer the measurement to a future CRP. This can be done using setTimeout or requestAnimationFrame.

For instance Code snippet 3:

refreshData(data, clear = false) {
if (!clear) {...} else {...}
data.forEach(datum => {
const element = document.createElement('data-table-row');
element.setAttribute('name', datum.name);
element.setAttribute('id', datum.id);
element.setAttribute('email', datum.email);
this._dataTable.prepend(element);
});
requestAnimationFrame(() => DataApp.emitEvent(this, 'refreshed-data', {
scrollHeight: this._dataTable.scrollHeight
}));
}
Code snippet 3: The async solution to forced reflow

Both code snippet 3 and code snippet 1 send the measurement after the DOM changes have been made. The difference is that code snippet 3 does that in the end of the CRP cycle, and then it uses the layout cache instead of recalculating it during the CRP cycle.

Figure 5 shows that we have managed to avoid forced layout by deferring the emitEvent call and the measurement to after the layout phase was complete..

Figure 5: A recording of the forced layout async solution. The emitEvent function is called after all the layout calculations have been made because it was deferred by requestAnimationFrame.

Summary

Layout reflow happens when we measure the DOM after we mutate it. It has severe performance implications and should be avoided as much as possible.

In this article, we saw an example for a code that has forced reflow and how to solve forced reflow.

If watching short videos fits you, I’ve created several Egghead videos about the subject including solutions for layout reflow usecases. Each video is around 1-2 minutes, so you can definitely just check it out 🙂

Thanks a lot for Hod Bauer for his thorough review of this article!

5 3 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments