Pixel pipes, this is one of the things that we write code for, and I’m sure a lot of people don’t know what it is, and there are a couple of articles on the web about it, but they’re not very good, so I’m going to talk about it in detail and how to optimize it.

About animation loading and people’s reactions

A smooth animation is about user experience (Retention)

delay The user response
0-16 milliseconds Most smart devices have a refresh rate of 60HZ, or 16 milliseconds per frame

(including the time it takes the browser to draw a new frame onto the screen),

The application is left with about 10 milliseconds to generate a frame.
0-100 milliseconds By responding to user actions within this time window, they feel they can get results immediately.

Any longer, and the link between the operation and the reaction breaks.
100-300 milliseconds Users experience slight appreciable delays.
300-1000 milliseconds Within this window, delay feels like a natural and ongoing part of the task.

For most users on the network, loading a page or changing a view represents a task.
+ 1000 milliseconds After 1 second, the user’s attention will be taken away from the task they are performing.
+ 10000 milliseconds Bye bye
  • For a response to an action, I recommend generally solving it in 100 milliseconds, and this applies to most inputs, whether they’re clicking a button, switching form controls, or launching an animation.
  • Always provide feedback for actions that take more than 500 milliseconds to complete, for exampleLoading.

About pixel Pipes

From a purely mathematical point of view, the budget for each frame is about 16 ms (1000 ms / 60 frames = 16.66 ms/frame). But because the browser takes time to draw the new frame onto the screen, it only has 10 milliseconds to execute the code.

If you don’t meet this budget, the frame rate drops and the content shakes on the screen. This phenomenon is commonly referred to as stalling and can negatively impact the user experience.

The time the browser spends drawing is the process of executing the pixel pipeline.

What is a pixel pipeline

A classic picture:

S Above is a pixel pipeline, which is the key point at which pixels are drawn on the screen.

  • JavaScript (code changes). In general, we useJavaScriptTo achieve some visual changes. Like jQueryanimateFunction to animate, sort a data set, or add some DOM elements to a page. Of course, in addition to JavaScript, there are other common ways to achieve visual changes, such as CSS Animations, Transitions, and the Web Animation API.
  • Style (Style calculation). The process is to use CSS matchers to calculate the changes to the elements and then calculate the final style of each element.
  • Layout calculation. When the Style rule is applied, the browser starts to calculate its position on the screen and how much space it takes up. However, changes to one element can affect another element and cause rearrangements, so layout changes are frequent and occur frequently.
  • Paint.. Drawing is a simple pixel fill that fills in the arranged styles. This includes text, colors, images, borders, shadows, and anything visible. Because the web style is a hierarchy, the drawing takes place at each level.
  • Composite. Because of the hierarchy, when the hierarchy is drawn, in order to ensure that the hierarchy is correct, the compositing operation is drawn on the screen in the correct hierarchy order to ensure that the rendering is correct, because a small hierarchical order error can cause style disorder.

Any part of the pipe can cause a blockage, so it’s important to know which part of the pipe is faulty and take appropriate measures.

Due to recent updates in browsers, many browsers have been able to separate render style changes and page draws into separate threads, which is out of our control, but no matter what changes, the pipeline is always going on, and not every frame is always going through every part of the pipeline. In fact, whether you’re using JavaScript, CSS, or web animation, there are usually three ways a pipe can run for a given frame when implementing visual changes.

Pipeline operation mode

JS/CSS— – >Style— – >Layout— – >Paint— – >Composite

This process is often referred to as browser reordering, where the geometry of an element (such as width, height, left or top position, etc.) is changed, and the browser will have to examine all other elements and then “automatically rearrange” the page. Any affected parts need to be redrawn, and the final drawn elements need to be composited, rearranging each step of the pipeline, and performance is significantly affected.

JS/CSS— – >Style— – >Paint— – >Composite

This is what is often called repaint, where you modify “paint only” properties (such as background images, text colors, or shadows) that do not affect the layout of the page, and the browser skips the layout but still performs the drawing.

JS/CSS— – >Style— – >Composite

This process does not rearrange and redraw, but only synthesize, that is, modify the transform and opacity properties to achieve animation, which greatly improves performance. It is most suitable for high pressure points in the application life cycle, such as animation or scrolling.

If you want to know which of the three versions will be triggered by changing any of the specified CSS properties, click here

The various pipeline jobs listed above vary in computational overhead, with some tasks more expensive than others, so let’s take a closer look at the different parts of the pipeline.

I’ll use some common problems as examples to explain how to diagnose and fix them.

How to optimize

JS/CSS (code changes)

JS is the most common way to change styles. There are a few things to note about changing animations using JS:

  • Use animation effects as much as possiblerequestAnimationFrameInstead of usingsetTimeoutorsetInterval
  • Due to theJSIf it is a single thread, please place the tasks that take a lot of time to runWeb WorkerExecute.
  • Use microtasks to do soDOMChange, if you don’t know what microtasks are, please clickhere
  • useChrome DevToolsTimelineJavaScriptProfilers to assess the impact of JavaScript

Using requestAnimationFrame

Most of the time, I think most people will use setTimeout or setInterval to perform an animation, but what’s the difference between these two functions and requestAnimationFrame?

It starts with the screen refresh rate.

Screen refresh rate

The screen refresh rate, which is the rate at which images are updated on the screen, is the number of times an image appears on the screen per second, measured in Hertz (Hz). For a typical laptop, this frequency is about 60Hz.

As a result, the computer screen is constantly updating the image 60 times per second, even when you’re looking at it and doing nothing. Why don’t you feel this change? That is because people’s eyes have visual retention effect, that is, the impression of the previous picture in the brain has not disappeared, followed by the next picture followed by the middle of the 16.7ms interval (1000/60≈16.7), so you will mistakenly think that the image on the screen is still. The screen is right. If the refresh rate is 1 refresh per second, the image on the screen will flicker severely, which can easily cause eye strain, soreness and dizziness.

The animation principles

According to the principle above, the image you see in front of you is refreshing at a rate of 60 times per second, so high that you don’t feel it refreshing. The essence of animation is to let the human eye see the visual effect of the change caused by the refresh of the image, and this change should be carried out in a coherent and smooth way. So how do you do that?

The screen with a refresh rate of 60Hz is refreshed every 16.7ms. Before each refresh, we move the position of the image to the left by one pixel, i.e. 1px. This way, each image on the screen will be 1px worse than the previous one, so you’ll see it move; Due to the visual stop effect of our human eyes, the impression of the current position of the image stays in the brain is not lost, and then the image is moved to the next position, so you can see the image in smooth movement, which is the visual effect of animation.

setTimeoutandsetInterval

After understanding the above concept, it is not difficult to find that setTimeout is actually to change the position of the image continuously by setting an interval time, so as to achieve the animation effect. However, we will find that animation implemented with seTimeout will stutter and shake on some machines with low configuration. There are two reasons for this phenomenon:

  • setTimeoutThe execution time is not fixed. inJavascript,setTimeoutThe task is placed in an asynchronous queue, and only after the main thread is finished does it check whether the task in the queue needs to be executed, sosetTimeoutThe actual execution time is usually later than the set time.
  • The refresh rate is affected by the screen resolution and screen size, so the refresh rate may vary from device to device, whilesetTimeoutYou can only set a fixed interval, which is not necessarily the same as the screen refresh time.

In both cases, the execution pace of setTimeout is inconsistent with the refresh pace of the screen, resulting in frame loss. So why does being out of step cause frame loss?

  • 0ms: The screen is not refreshed, waiting, setTimeout is not executed, waiting;

  • 10ms: The screen is not refreshed, waiting, setTimeout starts to execute and sets the image attribute left=1px;

  • 16.7ms: The screen starts to refresh, the image on the screen moves 1px to the left, setTimeout is not executed, and the waiting process continues;

  • 20ms: The screen is not refreshed, waiting, setTimeout starts execution and sets left=2px;

  • 30ms: The screen is not refreshed, waiting, setTimeout starts execution and sets left=3px;

  • 33.4ms: the screen starts to refresh, the image on the screen moves 3px to the left, setTimeout is not executed, and the waiting process continues;

As can be seen from the drawing process above, the screen does not update the left=2px frame, and the image directly jumps from 1px to 3px, which is a frame loss phenomenon, and this phenomenon will cause animation lag.

requestAnimationFrame

The biggest advantage of requestAnimationFrame over setTimeout is that the system decides when to execute the callback function. To be more specific, if the screen refresh rate is 60Hz, the callback function is executed every 16.7ms, if the refresh rate is 75 hz, the interval becomes 1000/75=13.3ms, in other words, the requestAnimationFrame moves at the pace of the system refresh. It ensures that the callback function is executed only once in each screen refresh interval, thus avoiding frame loss and animation stuttering.

In addition, requestAnimationFrame has the following two advantages:

  • CPU energy saving: When using setTimeout to achieve animation, when the page is hidden or minimized, setTimeout is still performing animation tasks in the background, because the page is not visible or unavailable at this time, refreshing animation is meaningless, it is a complete waste of CPU resources. RequestAnimationFrame is completely different. When the page is not active, the screen refresh task of the page is also suspended, so the requestAnimationFrame that follows the pace of the system will also stop rendering. When the page is active, The animation picks up where it left off, effectively saving CPU overhead.

  • Function throttling: In high frequency events (resize, Scroll, etc.), to prevent multiple function executions in a refresh interval, requestAnimationFrame is used to ensure that functions are executed only once in each refresh interval. This ensures smooth performance and saves function execution costs. It does not make sense if the function is executed more than once in a refresh interval, because the monitor is refreshed every 16.7ms and multiple draws do not show up on the screen.

Web Worker

Because JavaScript is single-threaded, running into a lot of computation problems can cause the entire page to get stuck, causing the page to feel very sluggish. In many cases, you can move pure computing work to the Web Worker, for example, if it doesn’t require DOM access. Data manipulation or traversal (such as sorting or searching) often lends itself well to this model, as do loading and model generation.

However, since the Web Worker does not have access to the DOM, if your work must be executed on the main thread, consider a batch approach that splits large tasks into microtasks, each of which takes no more than a few milliseconds, and runs within the requestAnimationFrame handler for each frame, and, You will need to use progress or activity indicators to ensure that the user knows the task is being processed, helping the main thread always respond quickly to user interactions.

Avoid microoptimizationsJavaScript

I know that many people have extreme optimizations where one function may be 10 times faster than another, such as requesting the offsetTop of an element faster than calculating getBoundingClientRect(), but the number of calls to such functions per frame is almost always small, Typically, it saves only a few tenths of a millisecond.

I’m not saying it’s not a good idea, but it’s not worth the effort compared to the improvement. That is to say, it takes a lot of effort to change the interface and break the structure of the code. I suggest that structure and stability are much more important than microoptimizations.

Style (Style calculation)

The terms rearrange and redraw are well known, and changing the DOM structure causes the browser to recalculate elements and, in many cases, rearrange entire pages or portions of pages (i.e., automatically rearrange them). This is called style computing.

Computing styles is actually a two-step process:

  1. Create a set of match selectors (the browser calculates which classes, pseudo-selectors, and ids to apply to the specified element)
  2. Gets all the style rules from the match selector and calculates the final style for this element

About 50% of the time spent calculating the style of an element is spent matching the selector, while the other half is spent building from the matched rule

There’s really nothing to write about in this section, but there are just two things to note:

  • Reduce the complexity of the selector
  • Reduce the number of elements whose styles must be computed

Reduce the complexity of the selector

Such as:

.box:nth-last-child(-n+1) .title {
  /* styles */
}
Copy the code

So this class, the browser is going to look and see if this is an element that has a title, and its parent happens to be minus the NTH child plus 1 element that has a box, right? Calculating this result can take a lot of time, depending on the selector used and the appropriate browser. This might be better:

.final-box-title {
  /* styles */
}
Copy the code

Of course, some styles will always use the first method, but I recommend using it as sparingly as possible.

Take a specific chestnut:

Here are the elements on the page:

<div class="box"></div>
<div class="box"></div>
<div class="box b-3"></div>
Copy the code

This is the CSS selector for writing

.box:nth-child(3)
.box .b-3
Copy the code

The more elements you find, the more time it takes to find. If.box: nth-Child (3) takes 2ms,.box. B-3 takes 1ms, and if there are 100 elements,.box: nth-Child (3) takes 200ms and.box. B-3 takes 100ms, the time difference is out.

In general, the worst overhead for calculating the style of an element is the number of elements multiplied by the number of selectors, because each element needs to be checked at least once against each style to see if it matches.

So try to minimize the number of invalid classes. It may not matter if you write them on a page, but it will be a burden on the browser, so I recommend using the BEM naming convention.

BEM is recommended

Encoding methods such as BEM (Block, element, modifier) actually incorporate the performance benefits of selector matching described above, as it recommends a single class for all elements and also includes the name of the class when hierarchy is required:

.list{}.list__list-item{}Copy the code

If we need some modifiers, like above where we want to do something special for the last child, we can add them as follows:

.list__list-item--last-child {}
Copy the code

BEM can be better organized with SASS

.list {
  &__list-item {
    &--last-child {}
  }
}
Copy the code

Layout = Layout

The process of layout is the process of rearrangement, rearrangement almost the entire page to recalculate the layout, the cost is obvious. The number and complexity of DOM affects performance.

Here are some tips to optimize the layout:

  • Avoid layout operations whenever possible
  • Use flexbox instead of floating layouts
  • Avoid forced synchronization of layouts
  • Avoid layout jitter

Avoid forced synchronization of layouts

Forced Synchronous Layout occurs when the CSS properties of the Layout section are triggered during the JavaScript code phase. For example, reading the offsetWidth value of an element forces the browser to update at this frame. The browser immediately calculates the style and layout, and then updates the view. At this point, the browser enters a read/write loop. With a graph:

Because it is abstract, let’s take a concrete chestnut:

Chestnut 1:

divs.forEach(function(elem, index, arr) {
  if (window.scrollY < 200) {
    element.style.opacity = 0.5; }});Copy the code

Setting opacity causes the browser to read scrollY/write opacity, and forEach causes the browser to perform this forcible synchronization loop all the time. Modified as follows:

const positionY = window.scrollY;

divs.forEach(function(elem, index, arr) {
  if (positionY < 200) {
    element.style.opacity = 0.5; }});Copy the code

The scrollY value is preread, and then the loop is written.

Chestnut 2:

divs.forEach(function(elem, index, arr) {
  if (elem.offsetHeight < 500) {
    elem.style.maxHeight = '100vh'; }});Copy the code

Same as before, read offsetHeight, write maxHeight.

Modified as follows:

if (elem.offsetHeight < 500) { // Read the property value first
  divs.forEach(function(elem, index, arr) { // Update the style again
    elem.style.maxHeight = '100vh';
  });
}
Copy the code

Chestnut 3:

var newWidth = container.offsetWidth;

divs.forEach(function(elem, index, arr) {
  element.style.width = newWidth;
});
Copy the code

The chestnut is correct, no problem.

Avoid layout jitter

Constant forced synchronization can cause layout jitter.

For the next chestnut, click and set the blue width to the same as the green:

The code is as follows:

const paragraphs = document.querySelectorAll('p');
const clickme = document.getElementById('clickme');
const greenBlock = document.getElementById('block');

clickme.onclick = function(){
  greenBlock.style.width = '600px';

  for (let p = 0; p < paragraphs.length; p++) {
    let blockWidth = greenBlock.offsetWidth;
    paragraphs[p].style.width = `${blockWidth}px`; }};Copy the code

So what’s the problem?

[p].style.width (s/S) : / / a/s/s/s/s/s/s/s/s/s/s/s/s/s Equivalent to the Layout step value, then interrupt, the next loop continues.

“Forced reflow is a likely performance bottleneck.”

The solution is simple:

clickme.onclick = function(){
  greenBlock.style.width = '600px';
  const blockWidth = greenBlock.offsetWidth;

  for (let p = 0; p < paragraphs.length; p++) {
    paragraphs[p].style.width = `${blockWidth}px`; }};Copy the code

Fetch greenblock. offsetWidth in advance, and then batch write.

Paint.

Drawing is the process of filling pixels that are eventually composited onto the user’s screen. It tends to be the longest running task in the pipeline.

In fact, what to say in this process, the following points need to pay attention to:

  • In addition totransformopacityChanging any property other than property always triggers drawing.
  • Drawing is usually the most expensive part of the pixel pipeline
  • Reduce the area of the drawing by elevating layers (z-index) and choreographing animations

Composite

Composition is the final link in the pixel pipeline, and composition is the process of putting drawn parts of a page together for display on the screen.

There are two key factors that affect the performance of a page in this regard: the number of synthesizer layers that need to be managed, and the properties that you use for animation.

  • z-indexToo many layers occupy more memory. Allocate them properly
  • Insist on usingtransformopacityProperty changes to animate, which does not trigger rearrangement and redraw.
  • usewill-changetranslateZPromote moving elements.

conclusion

Paint and Composite are the least mentioned in the whole article, as these are just the points that need attention.

People always ask, is it better to start with JavaScript or CSS?

From a pixel pipeline perspective, Layout changes can be expensive, whether you’re using JavaScript or CSS.

When writing JavaScript code, you can inadvertently cause forced synchronization and layout jitter.

In fact, the optimization of a project is not deliberately carried out, but accumulated in the process of bit by bit coding, so that optimization becomes your habit, and the code will naturally have optimized content.

Finally, I am sorry to promote the component library I wrote based on the Taro framework: MP-Colorui.

I will be very happy if I can star easily. Thank you.

Click here for documentation

Click here to get the GitHUb address