Make writing a habit together! This is my first day to participate in the “Gold Digging Day New Plan · April More text challenge”, click to see the details of the activity.

The browser’s rendering process

This article starts with the browser rendering process to explain backflow and redraw.

From the image above, we can see that the browser renders as follows:

  1. Parsing HTML, generatingThe DOM tree(DOM Tree), parse CSS, generateCSS tree(Style Rules)
  2. willThe DOM treeandCSS treeCombine, produceRender tree(Render Tree)
  3. According to the generated render tree, proceedbackflow(LayoutOr to callReflow), get the geometric information of the node (location.The size of the)
  4. According to the rendering tree and the geometric information obtained by backflow, the absolute pixel of the node is obtainedredraw(PaintingOr to callRepaint) :
  5. Send pixels to the GPU for display on the page (Display).

Let’s take a look at what happens to the important nodes in the rendering process

Generate Render Tree

To build the Render Tree(), the browser does the following:

  1. fromDOM TreeStarts traversing each visible node.
  2. For each visible node, findStyle RulesAnd apply them.
  3. Based on each visible node and its corresponding style, the combination is generatedRender Tree.

So in the first step, since we talked about traversing the visible nodes, we need to know what nodes are not visible. Invisible nodes include:

  • Some nodes that do not render output, such asscript,meta,linkAnd so on.
  • Some nodes are hidden by CSS, for exampledisplay:none. Take advantage ofvisibilityandopacityHidden nodes will still be displayed in the render tree.

Note:Render TreeContains only visible nodes

Layout or Reflow

By constructing the Render Tree, we combine the visible DOM nodes with their corresponding styles, but we also need to calculate their exact position and size in the device viewport. This stage of calculation is called backflow.

To figure out the exact size and location of each object on the site, the browser traverses from the document(root node) of the render tree, which we can represent in the following example:

<! DOCTYPEhtml>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>
Copy the code

As you can see, the first div has the display size of the node set to 50% of the viewport width, and the second div has its size set to 50% of the parent node, as shown below.

In the backflow phase, we need to convert the viewport to the actual pixel value according to the width of the viewport (figure below).

Redraw (Painting)

Finally, by constructing the Render Tree and the Layout stage, we know which nodes are visible, the style of the visible nodes and the specific geometric information (location, size), so we can convert each node of the Render Tree into an actual pixel on the screen. This stage is called Painting or Repaint.

Now that we know how the browser renders, let’s talk about when backflow redraws occur.

When reflux redraw occurs

As we have already seen, the backflow phase mainly computs the location and geometry of nodes, so when the page layout and geometry information changes, backflow is needed. For example:

  • When the page is first rendered (which is inevitable)
  • Add or remove visible DOM elements
  • The position of the element changes
  • The size of the element changes (including margins, inner borders, border size, height, width, etc.)
  • Content changes, such as text changes or an image being replaced by another image of a different size.
  • Browser window size changes (because backflow calculates element position and size based on viewport size)

Depending on the extent and extent of the change, large or small portions of the rendering tree need to be recalculated. Some changes can trigger a rearrangement of the entire page, such as when a scrollbar appears or when a root node is modified.

Note: Backflow must trigger redraw, and redraw does not necessarily backflow

Browser optimization mechanisms

Modern browsers are smart enough to optimize the reordering process by queuing changes and executing them in batches, since each reordering incurs an additional computation cost. The browser queues changes until a certain amount of time has passed or a threshold has been reached. But! Queue refresh is forced when you retrieve layout information from operations, such as when you access the following properties or use the following methods:

  • OffsetTop, offsetLeft, offsetWidth, offsetHeight
  • ScrollTop, scrollLeft, scrollWidth, scrollHeight
  • ClientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle()
  • getBoundingClientRect

All of the above attributes and methods need to return the latest layout information, so the browser has to clear the queue and trigger a backflow redraw to return the correct value. Therefore, it is best to avoid using the attributes listed above when modifying styles, as they all refresh the render queue. If you want to use them, it is best to cache the values.

Reduce backflow and redraw

All right, now for the big story of the day, with all this background and theory, let’s talk about how to reduce backflow and redraw.

Since backflow and redraw can be expensive, it is best to reduce the number of times it occurs. To reduce the number of occurrences, we can merge style and DOM changes multiple times and then dispose of them all at once.

Merge style changes

Consider this example

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
Copy the code

In the example, there are three style attributes that have been modified, each of which affects the element geometry and causes backflow. Of course, most modern browsers are optimized for this, so that only one backflow is triggered. However, if the above code executes while other code accesses the layout information (the layout information that triggered backflow above), it will result in three backflows.

Therefore, we can merge all the changes and deal with them at once. For example, we can do the following:

  • Using cssText
const el = document.getElementById('test'); 
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px; '; 
Copy the code
  • Modify the CSS class
const el = document.getElementById('test'); 
el.className += ' active'; 
Copy the code

Batch modify DOM

When we want to execute a batch insert node code:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text'; appendToElement.appendChild(li); }}const ul = document.getElementById('list');
appendDataToElement(ul, data);
Copy the code

If we did this directly, we would cause the browser to flow back once, since each loop would insert a new node.

The number of reflux redraws can be reduced by the following steps:

  1. Takes the element out of the document flow
  2. Modify it multiple times
  3. Bring the element back into the document.

The first and third steps of the process may cause backflow, but after the first step, any changes to the DOM will not cause backflow because it is no longer in the render tree.

We can take the DOM out of the document flow and optimize it in one of three ways:

1. Hide elements, apply changes, and display them again

This will cause two redraws while showing and hiding nodes

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text'; appendToElement.appendChild(li); }}const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
Copy the code

2. Use a documentFragment to build a subtree outside the current DOM and copy it back into the document

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
Copy the code

3. Copy the original element to a separate node, modify the node, and replace the original element.

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
Copy the code

For the above, I wrote a demo to test the performance before and after the changes. However, the results of the experiment were not so good.

The reason for this is that, as mentioned above, browsers use queues to store multiple changes and optimize them, so we don’t have to worry too much about these optimizations.

Avoid triggering synchronous layout events

As we mentioned earlier, when we access some attributes of an element, the browser forces the queue to be emptied and the layout to be synchronized. For example, if we wanted to assign the width of an array of P tags to the width of an element, we might write code like this:

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

This code looks fine, but it can cause a lot of performance problems. Each time through the loop, an offsetWidth property value of the box is read and used to update the width property of the P tag. This results in the browser having to validate the style update in the previous loop before it can respond to the style read in the current loop. Each loop forces the browser to refresh the queue. We can optimize as:

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

Also, I wrote a demo to compare the performance difference between the two, and the comparison gap is more obvious.

Complex animation effects taken out of the document flow using absolute positioning

For complex animation effects, which often cause backflow redraw, we can use absolute positioning to take it out of the document stream. Otherwise, it will cause frequent backflow of the parent element and subsequent elements.

Css3 Hardware acceleration (GPU acceleration)

Rather than thinking about how to reduce backflow redraw, we’d like to see no backflow redraw at all. This time, CSS3 hardware acceleration on the shining debut!!

How to use common CSS properties that trigger hardware acceleration:

  • transform
  • opacity
  • filters
  • Will-change

We can look at an example of the effect. I captured a period of backflow redrawing using Chrome’s Performance, and the actual result is shown below:

As you can see from the figure, very little backflow repainting takes place during the animation. You can do your own experiment if you’re interested.

Example code: HTML

<div class="demo"></div>
Copy the code
.demo {
  position: absolute;
  width: 100px;
  height: 100px;
  background: red;
  transition: transform 10s;
}
.demo:hover {
  transform: translate(500px, 500px);
}
Copy the code

Focus on

  • With CSS3 hardware acceleration, transform, opacity, and filters will almost never cause backflow redrawing
  • Other properties of animations, such as background-color, will still cause backflow redraw, but it can still improve the performance of those animations.

Css3 hardware acceleration pit

  • If you use CSS3 hardware acceleration for too many elements, it can lead to a large memory footprint and performance issues.
  • Anti-aliasing will not work on GPU rendering fonts. This is because GPU and CPU algorithms are different.