How does browser work step by step [latest] — optimization in the interaction stage (part 5)
Optimization in the interaction stage is about optimizing the frames.
When interacting with the page, a user triggers events. Some events modify the page layout and styles. Depending on the scripts, the browser may need to go through the rendering stage multiple times to continuously deliver new bitmaps or compositor frames to our screen.
FPS and lag
Human eyes can see up to 1,000 frames per second (FPS). In slow or static scenes, we rarely tell the difference beyond 30 FPS. However, it becomes noticeable in dynamic scenes, such as playing first-person shooter video games. Today, most monitors refresh rate is 60 FPS or 60 Hertz.
Ideally, the browser needs to complete the rendering stage and deliver a frame within 1/60 seconds. That is 16.67 milliseconds. If so, a user sees a smooth animation and doesn’t feel lagging.
In the interaction stage, usually, a page update is triggered by executing JavaScript. Most of the time, it could trigger reflow and repaint.
Reflow and repaint
What happens when a line of JavaScript changes an element height?
The height modification doesn’t affect the DOM tree. Instead, it requires style computation.
At the end of style computation, the renderer process reflects the height change to the layout phase, leading to the transformation of the element’s geometry information. Therefore, the layout tree needs to be generated.
The layout tree is the dependency of the remaining phases, so the renderer process needs to go through all steps.
This process is called reflow.
Many attributes checking could trigger reflow when called in JavaScript like “element.offsetLeft.” Here is a full list of them.
The worst case of the reflow is modifying DOM. An example is “document.body.appendChild(node).” The reflow process starts from the first phase, building the DOM tree.
How about changing the background color of an element?
Again, let’s start with the style computation. The new background color doesn’t modify the element’s geometry information, so the renderer process skips it. It doesn’t create a new layer either, so let’s skip the layer phase. At the paint phase, the renderer process needs to generate a new paint record to reflect the background color update. Then, it goes through the rest of the phases.
This process is called repaint.
Both reflow and repaint lower the rendering performance for two reasons:
- The reflow and repaint process is happening in the main thread, so it cannot take care of any events triggered by users’ interaction. When it happens, the users feel lagging.
- The computation process in the layout, layer, and paint phases are expensive.
Since the repaint skips the layout and layer phases, it is a relatively better option than reflow.
Are there any changes that don’t trigger reflow and repaint at all?
Yes. CSS animation is an excellent example.
A typical CSS animation uses a “transform” property. Modifying “transform” value skips the layout, layer, and paint phases, and starts with the tilling in the compositor thread.
Without occupying the main thread, CSS animation doesn’t block users’ interaction. It is the reason that you still see smooth CSS animation even if the page freezes.
Optimization in the interaction stage is about increasing the speed of frame generation
JavaScript execution, reflow, and repaint could slow down the frame generation.
When executing, JavaScript are running in the main thread. The idea here is to use the main thread as little as possible.
Reduce the length of time executing JavaScript
For example, a significant function could take hundreds of milliseconds to complete. It blocks the main thread and lowers the performance.
We can separate the function to smaller ones, so each of them doesn’t take long. The browser helps optimize the tasks when running the functions.
Web Worker is another option. You can operate it as an independent thread in the renderer process. When scripts run in the Web Worker, the main thread is free. If a piece of JavaScript is not visiting DOM and stylesheets, you can move it to the Web Worker.
Avoid the reflow and repaint when executing JavaScript
When the DOM tree is modified, the renderer process will recalculate the style and layout. Usually, the computation runs asynchronously in another task.
Let’s take a look at an example.
The first task completes the JavaScript execution. Then another task runs asynchronously to compute the style and layout.
What if we check the element height at the end of the script?
When evaluating the element “offsetHeight,” the value is still the old one because the renderer process has not yet computed the style and layout. The renderer process starts the calculation synchronously so it can receive the updated value.
In this case, we force the style and layout computation happening in the JavaScript execution task. The calculation blocks the main thread until the execution completes.
What is worse? We evaluate the attributes in a for-loop. The previous process happens continuously until the end of the execution. Most of the time, a noticeable delay occurs on the page.
In a real-life project, it is hard to avoid evaluating the attributes entirely. However, we can try to minimize usage.
Using CSS animation and “will-change”
CSS animation doesn’t use the main thread at all, so we can use it as much as possible.
Meanwhile, we can attach the “will-change” attribute to the animated elements. An element with a “will-change” is rendered on a stand-along layer in the layer tree, further optimizing the frame generation in the compositor thread.