Editor’s note: Berwin is a member of the W3C Performance Working Group and a senior front end engineer for 360 Navigation. Author of Vue.js (in press).

For a while, I focused on Web performance; An important topic in this area is how to make web pages more silky.

To make a web page silky, first of all, we need a standard to determine what is silky. Secondly, we should accurately measure the performance data of the web page; Finally, use effective methods to make your web pages silky smooth.

This article will introduce these three aspects in detail.

1. RAIL

What kind of web page is silky? We need a standard to help determine whether our web pages are silky or not.

The Chrome team came up with a user-centric performance model called RAIL, which provides engineers with a goal that users will feel smooth as long as they reach the goal of a web page; It breaks down the user experience into key actions, such as click, load, etc. Give these actions a goal, such as how long it takes to give feedback after a button is clicked.

RAIL divides the behaviors that affect performance into four aspects: Response, Animation, Idle and Load. Yes, the name RAIL comes from the first letter of those four words, which is easy to remember.

1.1 The response(Response)

Research shows that a response to a user’s input operation within 100ms is usually considered as an immediate response by humans. Any longer, the connection between the action and the reaction will be broken, and people will perceive its action as delayed. For example, if a user clicks a button and gets a response within 100ms, the user will feel that the response is timely and will not notice any delay.

1.2 animation(Animation)

Most devices now refresh at 60Hz, or 60 screens per second. As a result, web animations can run at 60FPS and feel smooth.

F(Frames) P(Per) S(Second)

(1 second = 1000 ms) / 60 frames = 16.66 ms/frameCopy the code

However, it usually takes the browser some time to draw each frame onto the screen (including style calculation, layout, drawing, composition, etc.), so we usually only have 10 milliseconds to execute the JS code.

1.3 free(Idle)

For better performance, we usually take advantage of the Idle Period of the browser to do low-priority things. For example, pre-request some data that may be used later or report analysis data in an idle period.

RAIL specifies that a task running in an idle cycle should not exceed 50ms. Of course, not only RAIL, but the W3C Performance Working Group’s Longtasks standard also specifies that a task running in an idle cycle over 50ms is a long task. So where does this number come from?

Browsers are single-threaded, which means that the main thread can only handle one task at a time. If one task takes too long, the browser can’t perform other tasks, and the user feels that the browser is stuck because his input is not getting any response.

To respond within 100ms, limiting the idle cycle to 50ms means that even if the user’s input occurs at the beginning of the idle task execution, the browser still has the remaining 50ms to respond to the user’s input without any noticeable delay by the user. As shown in Figure 1-1:

In fact, neither idle tasks nor other high-priority tasks should take more than 50ms to execute.

1.4 loading(Load)

If the web page can’t load and the user can see the content in less than a second, the user’s attention will be distracted. The user feels like he’s being interrupted, and if the page doesn’t open for 10 seconds, the user gets frustrated, gives up on what they want to do, and they may never come back.

1.5 summary

With RAIL, we can tell if our web page is silky or not. RAIL provides some indicators from the perspective of user perception. As long as our web pages meet the standards, our web pages are silky and users will feel that our web pages are smooth.

RAIL Key indicators The user action
Response Less than 100 ms Click the button.
Animation Less than 16 ms Scroll pages, drag fingers, play animations, etc.
Idle Less than 50 ms The user is not interacting with the page, but the main thread should be sufficient to handle the next user input.
Load 1000ms The user loads the page and sees the content.

2. Pixel pipes

Pixel pipes are the soul of silky web pages, and the techniques we’ll cover later are all about them.

The image above is a pixel pipeline. Usually we modify some style with JS, then the browser calculates the style, then does the layout, then draws, and finally merges the layers together to complete the rendering process, each step of which can cause the page to get stuck.

Note that not all style changes need to go through these five steps. For example, if you change the geometry of an element (width, height, etc.) in JS, the browser will need to go through all five steps. But if you just change the color of the text, the Layout can be skipped, as shown below:

Except for the final composition, the first four steps can be skipped in different scenarios. For example, CSS animation can skip JS calculations, it does not need to execute JS.

Css-triggers1 shows which steps in the pixel pipeline are triggered when different CSS properties are changed.

To put it simply, the more steps a pixel pipeline goes through, the longer the rendering time will be, and a single step may take a long time for some reason; So whether it’s multiple steps or a single step that takes a long time, the overall rendering time will end up being longer. The longer the overall time, the more likely it is to exceed the target set by RAIL.

A simple example: if you render a web animation at 60FPS, the animation will not lose frames. Suppose rendering pipeline layout and draw took 10 ms, then add style calculation and synthesis time, is left to the JS processing animation time is only a few milliseconds, if JS performs more than a few milliseconds the animation, the amount of time each frame will be more than 16 ms, animation will lost frame at this time, the user with the naked eye can see clearly the card.

Of course, even if you can ensure that the total time of each frame is less than 16ms, there is still no guarantee that you will not lose frames. We’ll talk more about that later.

3. How to make animation more silky

Animations need to hit 60FPS to become silky, in this section we’ll show you how to keep animations stable at 60FPS without losing frames.

3.1 Use Chrome Developer Tools to measure animation performance

When evaluating animation performance, it is usually necessary to evaluate the overhead of pixel pipes frame by frame; Use Chrome Developer tools to help us take accurate measurements.

In Chrome Developer Tools, click on the Performance panel and then select the Screenshots check box. As shown in Figure 3-1:

Then click the record button, after recording, click the stop button to capture the performance data of the current page. As shown in Figure 3-2:

The captured results are shown in Figure 3-3:

We can zoom in on the main thread to see exactly what tasks the browser performed in each frame and how long each task took. See Figure 3-4.

As you can see from the image above, each frame rendered by the browser performs the same tasks as the pixel pipeline we described earlier. In the image above, there is no JS running because it is a CSS animation, but every frame needs to be styled, laid out, drawn and composited.

3.2 How to make JS animation more silky

JS animation is the use of timer to keep the execution of JS, by modifying the style in JS to complete the web animation; In order to ensure smooth animation, it takes up to 16ms per frame from JS execution to the final browser display, so that the animation can reach 60FPS.

As shown in Figure 3-4, it takes time for the browser to calculate the style, layout, and drawing even when JS is not executed. Therefore, you need to leave enough time (6ms) for the browser to do these things. Now, the JS execution time is only 10ms.

Once the JS running time exceeds 10ms, it is very likely that the entire pixel pipeline of this frame will take more than 16ms, thus failing to reach 60FPS, but you think that as long as the JS running time is less than 10ms, you can guarantee no frame loss? Naive~

3.2.1 userequestAnimationFrame

Even if you can ensure that the total time of each frame is less than 16ms, there is no guarantee that frames will not be lost, depending on how JS execution is triggered.

Suppose you use setTimeout or setInterval to trigger JS execution and modify styles to cause visual changes; There is a situation where setTimeout or setInterval has no way to guarantee when the callback will execute, and it may execute in the middle of each frame or at the end of each frame. Therefore, even if we can ensure that the total time of each frame is less than 16ms, if the execution time is in the middle or at the end of each frame, the final result is still that there is no way to change the screen every 16ms. See Figure 3-6.

In other words, even if we can ensure that the total time of each frame is less than 16ms, if the timer is used to trigger the animation, it will still cause the animation to lose frames due to the uncertain timing of the timer trigger. There is currently only one API in the entire Web that can solve this problem, requestAnimationFrame, which ensures that callbacks are consistently fired at the beginning of every frame. As shown in Figure 3-7:

3.2.2 avoid FSL

FSL (Forced Synchronous Layouts)

JS is executed, then styles are modified in JS to cause style calculations, and the style changes trigger layout, drawing, and composition. But JavaScript can force the browser to execute the layout ahead of time, which is called F (force) S (sync) L (layout).

FSL is often caused by accident, as shown in the following code:

box.classList.add('big');
const width = box.offsetWidth;
Copy the code

The code modifies the style of the element by adding a new class, and then reads the width of the element using offsetWidth. At first glance, this code seems fine, but it causes FSL.

When JavaScript is running, all the layout values that were rendered in the last frame are known, and we can get the values using syntax like offsetWidth; But this frame has not been rendered by the browser, so we use syntax like offsetWidth to read the width of the element, so in order for the browser to tell us the width value, it has to calculate the width, which is the layout. As Figure 3-8 shows, layout comes ahead of style calculations.

So the correct thing to do is to get the width first and then change the style:

const width = box.offsetWidth;
box.classList.add('big');
Copy the code

It seems that even if FSL is triggered it is only a change in the order of the pipes, and the effect does not seem to be that big. 🤔

A single FSL does have a small impact on performance, but if layout jitter is triggered, the impact can be significant. Look at the following code:

const container = document.querySelector('.container');
const boxes = document.querySelectorAll('p');

for (var i = 0; i < boxes.length; i++) {
  // Read a layout property
  const newWidth = container.offsetWidth;
    
  // Then invalidate layouts with writes.
  boxes[i].style.width = newWidth + 'px';
}
Copy the code

The above code is used to batch modify the width of N P elements; In the loop we first get the width of the container element and then style the P element. This causes the browser to do the layout and then calculate the style. Each time we change the style, the layout we just executed is invalidated because we change the style again, so the next time the loop reads the width, the browser executes the layout again, and so on until the loop ends. During a loop, the browser keeps executing invalid layouts, which is called Layout Thrashing; The performance problems caused by such errors are very high.

If we accidentally trigger FSL, Chrome Developer Tools will give us a red line, as shown in Figure 3-9:

At the same time, there will be a red triangle in the upper right corner of the task. We can zoom in on the task for further inspection, as shown in Figure 3-10:

If you want to see the Demo, you can click on me 2. Click on the button in the Demo to make the width of the P label longer.

To avoid layout jitter, we can place the code that reads the width of the element outside the loop. The code is as follows:

const container = document.querySelector('.container');
const boxes = document.querySelectorAll('p');

// Read a layout property
const newWidth = container.offsetWidth;

for (var i = 0; i < boxes.length; i++) {    
    // Then invalidate layouts with writes.
    boxes[i].style.width = newWidth + 'px';
}
Copy the code

If you want to see the Demo, you can click on me 3, and you can see that this Demo is exactly the same as the previous one, even though you can’t tell which one is faster with the naked eye, because there are fewer DOM elements, so the total time is shorter, but you can capture performance data using Chrome developer tools.

As can be seen in Figure 3-11, the total time of this frame after optimization is 4.7ms, while that before optimization is 101ms, as shown in Figure 3-12:

The elapsed time per frame after optimization is 21.7 times faster than before optimization, which is an amazing number.

3.3 How to make CSS animation more silky

CSS animations usually use @keyFrame or Transition in combination with style changes to achieve visual changes. We can also make CSS animations smoother by reducing the number of steps in the pixel pipeline and the amount of time each step takes.

The optimization methods of CSS animation introduced in this section also apply to JS animation, but the optimization methods of JS animation introduced in the previous section do not apply to CSS animation, they are containment relations.

Paint usually takes a long time, so you can use Chrome developer tools to see what area is being painted. Open developer tools and press Esc on your keyboard. On the panel that appears, switch to the “Rendering” TAB, and then select “Paint Flashing”. As shown in Figure 3-13.

For example, when a page is drawn, we can see a flashing green light in the drawing area on the screen after the Paint flashing light is enabled. See Figure 3-14.

As shown in Figure 3-14, when we turn on draw flicker, green flicker will appear in the drawing area. You can click me to view Demo4.

When we see an area that we think should not be drawn, we should look further and undraw the area.

How can you avoid drawing? The answer is: layers.

In fact, when the browser renders a page, it can divide the page into many layers. Similar to PhotoShop, an image in PotoShop is composed of many layers, and the browser will actually display a page composed of many layers. As shown in Figure 3-15:

By elevating the changing elements to a separate layer, you don’t need to draw any more. All the browser needs to do is to merge the two layers together.

If you click on the Demo address above and turn on draw flicker, you will see that no flicker occurs because the browser is not drawing. If you look at the Layers panel, you’ll see something like this, as shown in Figure 3-16:

When we capture Performance data using the Performance panel, we find that Paint is missing. As shown in Figure 3-17:

The best way to create a layer is to use will-change, but some browsers that don’t support this property can use transform 3D (translateZ(0)) to force the creation of a new layer.

On the Chrome Developer Tools “Rendering” TAB, select “Layer Borders”. You can see what composition layers are in the page. The composite layer will use an orange border, as shown in Figure 3-18:

In order to reduce drawing, new layers can be added, but layer management is also expensive, so to avoid abuse, usually need to make a case-by-case analysis, appropriate choice.

In my previous Demo, I changed the left attribute of the element to move the block. This does not avoid the need for layout operations. The best way to do this is to use the transform attribute, which is handled by the synthesizer alone, so using this attribute can avoid layout and drawing.

conclusion

RAIL helps us determine which pages are silky, and developer tools allow us to capture performance data more accurately.

JS animation should ensure that 6ms time is reserved for the browser to process the pixel pipeline, and its execution time should be less than 10ms to ensure that the overall running speed is less than 16ms. However, the timing of the animation is also important, timers cannot reliably trigger the animation, so we need to use requestAnimationFrame to trigger the JS animation. At the same time, we should avoid all FSL, which has a significant impact on performance.

CSS animations can be done by lowering the drawing area and using the Transform property. We also need to manage layers well. Since drawing and layer management are both expensive, we often need to make trade-offs and make the best choice for each situation.