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:
A reflow looks more like this:
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.
element.style('height', 500); console.log(element.style('height'));
Exercise – Create a Forced Reflow
- Clone the following repository: https://github.com/YonatanKra/performanceWorkshop
git clone https://github.com/YonatanKra/performanceWorkshop
- Type: checkout layout-reflow-1
- Run yarn or npm i
- Run yarn demo or npm run demo
- Open the app in the browser: http://localhost:3000
- Query the server (just use the input field at the top)
- Start monitoring
- Click the sorting buttons
- 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:
Let’s compare it to the CRP recording of a reflow-free code:
The reflow in Figure 3 happens because a simple line that was added to the code. In the data-table.component.js file:
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?
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:
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
For instance Code snippet 3:
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..
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!