In the browser rendering process and Performance Optimization article, we learned about key browser rendering paths and how to optimize page loading speed. In this article, we focus on improving browser rendering performance (the browser does layout calculations, draws pixels, and so on) and efficiency.

Many web pages are used looks very cool animation interact with the user, the animation effects significantly improve the user experience, but if for performance reasons lead to the animation frames per second is too low, it will let the user experience worse (if a cool animations to run always caton or often look response is slow, All of which makes users feel terrible).

A smooth animation needs to be kept at 60 frames per second, which translates to milliseconds. The browser needs to render in about 10 milliseconds. Understanding the browser’s rendering process and optimizing it for performance bottlenecks can make your projects interactive and smooth.

SylvanasSun([email protected]) Please be sure to put this paragraph in the article reprinted beginning (keep hyperlinks). This article first from SylvanasSun Blog, the original links: SylvanasSun. Making. IO / 2017/10/08 /…

Pixel pipe


A pixel pipeline is the process by which a browser draws a render tree into pixels. Each region of the pipe is likely to stall, meaning that if a region of the pipe changes, the browser will automatically rearrange it and then redraw the affected region.

Pixel pipe

  • JavaScript: This area refers to the methods used to animate effects, such as JQuery animate functions, sorting a data set, or dynamically adding DOM nodes. Of course, you can also use other methods to animate, such as CSS Animation, Transition, and Transform.

  • Style: This area is the Style calculation phase, where the browser calculates which CSS rules apply to which nodes based on selectors (CSS selectors, such as.td), and then calculates the final Style for each node and applies it to the nodes.

  • Layout: This area is the Layout calculation phase, during which the browser calculates how much space a node should occupy and its position on the screen based on its style rules.

  • Paint: This area is the draw phase, where the browser creates a list of draw calls and then fills in the pixels. The draw phase involves text, colors, images, borders, and shadows, including basically every visible part. Drawing is usually done on multiple layers (the term “layer” is familiar to anyone who has used Photoshop or other photo editing software, but the meaning of layer is similar).

  • Composite: This area is the Composite phase where the browser draws multiple layers onto the screen in the correct order.

Suppose we change a geometry property (such as width, height, etc.), and the Layout stage is affected. The browser must check all the elements in the other areas, and then automatically rearrange the page, redrawing any affected parts. And the final drawn elements need to be recomposed (in short, the entire pixel pipeline needs to be reexecuted).

If we change only properties that do not affect the layout of the page, such as background images, text colors, and so on, the browser skips the layout stage, but it still needs to be redrawn.

Alternatively, if we change only one property that does not affect the layout or the drawing, the browser will skip the layout and drawing phases, which is obviously the least performance expensive change.

To find out how each CSS attribute will affect which phase, go to CSS Triggers, a website detailing which phase each CSS attribute affects.

The animation is implemented using the RequestAnimationFrame function


We often use JavaScript for animations, but bad timing or long running JavaScript can be the cause of your performance degradation.

Avoid using setTimeout() or setInterval() for animation. The main problem with this is that the callback will run at a certain point in the frame, which may be right at the end (losing frames and causing stuttering).

Some third-party libraries still use setTimeout()&setInterval() for animation, which can cause a lot of unnecessary performance degradation. For example, in older versions of JQuery, if you’re using JQuery3, don’t worry about this. The requestAnimationFrame() function is used to implement the animation. However, if you are using a previous version of JQuery, you need jquery-RequestAnimationFrame to replace setTimeout() with requestAnimationFrame().

Reading this, you must be curious about requestAnimationFrame(). To get a smooth animation, we want visual changes at the beginning of each frame, and the way to ensure JavaScript runs at the beginning of each frame is to use the requestAnimationFrame() function, which is essentially the same as setTimeout(), RequestAnimationFrame () is used as follows:

function updateScreen(time) {
    // This is your animation function
}

// Put your animation function in requestAnimationFrame() as a callback
requestAnimationFrame(updateScreen);Copy the code

RequestAnimationFrame () is not supported in all browsers, such as Internet Explorer 9 (again, the evil Internet Explorer), but it is generally supported in modern browsers. If you need to work with older browsers, you can use the following functions.

/ / this code intercepting since Paul Irish: https://gist.github.com/paulirish/1579671
(function() {
    var lastTime = 0;
    var vendors = ['ms'.'moz'.'webkit'.'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] | |window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    // If the browser does not support it, use setTimeout()
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0.16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } ());Copy the code

Web Workers


We know that JavaScript is single-threaded, but browsers are not single-threaded. JavaScript runs on the main thread of the browser, which happens to run in conjunction with style calculations, layouts, and many other rendering operations in many other situations, and if JavaScript runs for too long, it blocks these subsequent operations, resulting in frame loss.

Using the Timeline feature of Chrome Developer Tools allows us to see the running time of each JavaScript script (including sub-scripts) to help us find and break performance bottlenecks.

The data were taken from the nuggets

After finding JavaScript scripts that affect performance, we can optimize through Web Workers. Web Workers is a standard from HTML5 that allows JavaScript scripts to run on background threads (similar to creating a child thread) without affecting the page in the main thread. However, threads created using Web Workers cannot manipulate the DOM tree. (This is why Web Workers didn’t overturn the single-threaded JavaScript design. JavaScript has always been single-threaded mainly because to avoid synchronization problems when multiple scripts manipulate the DOM tree, This adds a lot of complexity), so it’s only good for purely computational tasks (sorting data, traversing, etc.).

If your JavaScript has to be executed in the main thread, you have to go the other way. Split a large task into several smaller tasks (each taking no more than a few milliseconds) and run them in the requestAnimationFrame() function per frame:

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
  var taskFinishTime;

  do {
    // Pop tasks from the list
    var nextTask = taskList.pop();

    // Execute the task
    processTask(nextTask);

    // Continue if there is enough time for the next task
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);

  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);

}Copy the code

Creating a Web Workers object is as simple as calling the Worker() constructor and passing in the URI that specifies the script. Modern major browsers support Web Workers, with the exception of Internet Explorer (again, the evil Internet Explorer), so we also need to check browser compatibility in the example code below.

var myWorker;

if (typeof(Worker) ! = ="undefined") {
    // Support Web Workers
    myWorker = new Worker("worker.js");
} else {
    // Web Workers are not supported
}Copy the code

Web WorkersPasses between the main threadpostMessage()Function to send a message, usingonmessage()Event handlers respond to messages (the main thread and child threads do not share data, but interact by copying data).

main.js: 
// Send data in the main thread js to the myworker-bound js script thread
myWorker.postMessage("Hello,World");
console.log('Message posted to worker');

worker.js:
// The onmessage handler allows us to, at any time,
// Once the message is received, you can execute some code that uses the message itself as the data attribute of the event.
onmessage = function(data) {
    console.log("Message received from main script.");
    console.log("Posting message back to main script.");
    postMessage("Hello~");
}

main.js:
// The main thread uses onMessage to receive messages
myWorker.onmessage = function(data) {
    console.log("Received message: " + data);
}Copy the code

If you need to terminate a running worker immediately from the main thread, you can call worker terminate() :

myWorker.terminate();Copy the code

MyWorker is killed immediately, without any chance for it to finish the rest of its work. The worker thread can also be closed by calling close() :

close();Copy the code

For more Web Workers use method, please refer to the Using Web Workers – Web APIs | MDN.

Reduce the complexity of style calculations


Every change to the DOM and CSS causes the browser to recalculate and, in many cases, to recalculate the layout of a page or part of a page.

The first part of calculating the style is to create a set of match selectors (which are used to calculate which nodes apply which styles), and the second part involves getting all the style rules from the match selectors and figuring out the final style of the nodes.

You can speed up style calculations by reducing the complexity of the selector.

Here is a complex CSS selector:

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

If the browser wants to find a node that applies this style, it needs to find a node that has the.title class, and then its parent is exactly minus n child elements +1 node with the.box class. It may take a lot of time for the browser to calculate this result, but we can change the expected behavior of the selector to a class:

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

By modularizing CSS naming (reducing the complexity of the selectors) and then simply letting the browser match the selectors to the nodes, the browser will be much more efficient at styling.

BEM is a modular CSS naming convention that uses this method to organize CSS in a way that is not only structurally clear, but also helps browsers find styles.

BEM is Block,Element,Modifier. It is a component-based approach to development, and the idea behind it is to divide the user interface into separate chunks. This makes it easy and fast to develop even with complex UIs, and modularized approaches improve code reuse.

A Block is a self-contained page component (that can be reused) and is named like a Class name. Button is a Block that represents

.button {

    background-color: red;

}



<button class="button">I'm a button</button>Copy the code

An Element is a compound part of a Block that cannot be used alone. You can think of an Element as a child of a Block.

<! -- 'search-form' is a block -->

<form class="search-form">

<! -- 'search-form__input' is an element in the 'search-form' block -->

    <input class="search-form__input">



<! -- 'search-form__button' is an element in the 'search-form' block -->

    <button class="search-form__button">Search</button>

</form>Copy the code

Modifier is the entity used to define the appearance, state, or behavior of a Block or Element. Suppose we have a new requirement for the button’s background color to be green, then we can use the Modifier to extend. Button:

.button {

    background-color: red;

}



.button--secondary {

    background-color: green;

}Copy the code

For those who are new to BEM, this naming may seem strange, but the idea of modularity and maintainability is important to BEM, and the naming can be changed in any way you like. Due to space limitations, this article will not continue to discuss BEM, interested children can refer to the official BEM documentation.

Avoid forced layout synchronization and layout jitter


Every time the browser does a layout calculation, it almost always applies to the wholeDOMIf there are a large number of elements, it will take a long time to calculate the position and size of all the elements.

Therefore, we should avoid dynamically changing geometry properties (width, height, etc.) at run time, because these changes will cause the browser to recalculate the layout. If you can’t avoid it, use Flexbox first, which minimizes the overhead of layout.

Forcing a synchronous layout is using JavaScript to force the browser to execute the layout ahead of time. It’s important to understand that at JavaScript runtime, all the old layout values from the previous frame are known.

Take the following code, which prints the height of the element at the beginning of each frame:

requestAnimationFrame(logBoxHeight);

function logBoxHeight() {
  console.log(box.offsetHeight);
}Copy the code

However, if you change the style before requesting the height, you have a problem. The browser must apply the style and then calculate the layout before returning the correct height. This is unnecessary and incurs very high overhead.

function logBoxHeight() {
  box.classList.add('super-big');

  console.log(box.offsetHeight);
}Copy the code

Instead, take advantage of the fact that the browser can use the layout values of the previous frame before performing any write operations:

function logBoxHeight() {
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}Copy the code

If forced synchronous layouts occur in succession, layout jitter will occur. The following code loops through a set of paragraphs and sets the width of each paragraph to match the width of an element named “box.”

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px'; }}Copy the code

The problem with this code is that each iteration reads box.offsetwidth and then immediately uses this value to update the width of the paragraph. In the next iteration of the loop, the browser must account for the fact that the style is updated (box.offsetwidth was requested in the previous iteration), so it must apply the style changes and then perform the layout. This results in a forced synchronous layout for each iteration, and the correct practice is to read the values first and then write them.

// Read.
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = width + 'px'; }}Copy the code

To easily solve this problem, FastDOM is used for bulk reads and writes, which prevents forced layout synchronization and layout jitter.

Animate using properties that do not trigger layout and drawing


In the Pixel Pipeline section, we found that one property was modified to skip the layout and drawing phases, which obviously reduces performance overhead. Only two properties currently qualify: transform and opacity.

Note that when using transform and opacity, the element on which these properties are changed should be on its own layer, so we need to create a new layer for the element to be animated separately (the advantage of this is that redrawing on this layer can be handled without affecting elements on other layers). If you’ve ever used Photoshop, you understand the convenience of working with multiple layers).

The best way to create a new layer is to use the will-change attribute, which tells the browser how the element will change so that the browser can optimize the element before it actually changes.

.moving-element { will-change: transform; } // For browsers that don't support will-change but benefit from layer creation, use (abuse) 3D deformations to force the creation of a new layer. Moving-element {transform: translateZ(0); }Copy the code

But don’t think thatwill-changeYou can improve performance just abuse it, use itwill-changePre-optimizing and creating layers requires extra memory and administrative overhead, which is more than worth it.

reference


  • Web | Google Developers

  • Using Web Workers – Web APIs | MDN

  • will-change – CSS | MDN

  • Quick start / Methodology / BEM