Author: Chen Jigeng

Backflow and redraw are two words that every Web developer hears a lot, but many people may not be quite sure what they do. Recently, I have time to do some research on it, read some blogs and books, sort out some content and combine some examples, and write this article. I hope it can help you.

The reading time is about 15 to 18 minutes

Browser rendering process

This article covers backflow redrawing from the beginning to the end of the browser rendering process. If you want to see how to reduce backflow and redrawing and optimize performance, you can skip to the next section. (This rendering process comes from MDN)

From the above diagram, we can see that the browser rendering process is as follows:

  1. Parse HTML, generate DOM tree, parse CSS, generate CSSOM tree
  2. Create a Render Tree by combining the DOM Tree with the CSSOM Tree.
  3. Layout(backflow): Perform the backflow according to the generated rendering tree to get the geometric information (location, size) of the node.
  4. Painting: Derive the absolute pixels of the node from the geometry of the render tree and the backflow
  5. Display: Sends pixels to the GPU for Display on the page. (This step is actually a lot of content, such as the GPU will be multiple composition layer into a single layer, and displayed in the page. Css3 hardware acceleration works by creating a new composition layer, which we won’t expand on here, but we’ll blog about it later.)

The rendering process seems simple enough, so let’s take a look at what we’re doing at each step.

Generate the render tree

To build the rendered tree, the browser does the following:

  1. Each visible node is traversed from the root node of the DOM tree.
  2. For each visible node, find the corresponding rules in the CSSOM tree and apply them.
  3. According to each visible node and its corresponding style, the combination generates the render tree.

In the first step, since we’re talking about traversing visible nodes, we need to know what nodes are not visible. Invisible nodes include:

  • Nodes that do not render output, such as script, meta, link, etc.
  • Some nodes are hidden by CSS. Such as display: none. Note that nodes hidden by using visibility and opacity are still displayed in the render tree. Only nodes with display: None are not displayed in the render tree.

From the above example, we can see that the span tag has a display: None style, so it does not end up in the render tree.

Note: The render tree contains only the nodes that are visible

backflow

In the previous section, we constructed the render tree, where we combined the visible DOM nodes with their corresponding styles, but we also needed to calculate their exact location and size within the device viewport, and the stage of this calculation was backflow.

To figure out the exact size and location of each object on the site, the browser starts at the root of the render tree, which we can represent as the following example:


      
<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

We can see that the first div sets the display size of the node to 50% of the viewport width, and the second div sets its size to 50% of the parent node. In the backflow stage, we need to convert the viewport width to the actual pixel value. (Figure below)

redraw

Finally, by constructing the render tree and the reflux stage, we know which nodes are visible, the style of the visible nodes and the specific geometric information (location, size), so we can transform each node of the render tree into an actual pixel on the screen. This stage is called redraw node.

Now that we know how the browser renders, let’s talk about when a backflow redraw occurs.

When reflux redraw occurs

As we know earlier, the backflow phase is mainly about computing node location and geometry information, so when the page layout and geometry information changes, you need to backflow. For example:

  • Add or remove visible DOM elements
  • The position of the element changes
  • Element size changes (margin, inner border, border size, height, width, and so on)
  • Content changes, such as text changes or an image is replaced by another image of a different size.
  • The first time the page is rendered (which I can’t avoid)
  • Browser window size changes (because backflow calculates the position and size of elements based on viewport size)

Note: backflow always triggers redraw, and redraw does not always backflow

Depending on the scope and extent of the changes, large or small parts of the rendered tree need to be recalculated, and some changes can trigger reordering of the entire page, such as when the scrollbar appears or when the root node is changed.

Optimization mechanisms for browsers

Modern browsers are smart enough to optimize the reordering process by queuing changes and executing them in batches, since each reordering incurs additional computational costs. The browser queues changes until a certain amount of time has passed or a threshold has been reached. But! Queue refreshes are enforced when you retrieve layout information, 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
  • Specific can access the site: gist.github.com/paulirish/5…

All of the above properties 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, when changing styles, it is best to avoid using the attributes listed above, 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 our highlight of the day, with so much background and theory, let’s talk about reducing backflow and redrawing.

Minimize redraws and rearrangements

Since redraws and rearranges can be expensive, it is best to reduce their occurrence. To reduce this occurrence, we can merge the DOM and style changes several times and then dispose of them all at once. Consider this example

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

In this example, there are three style attributes that have been modified, each of which affects the geometry of the element, causing reflux. Of course, most modern browsers optimize it so that only one reorder is triggered. However, if some other code accesses the layout information (the layout information that triggers backflow above) in an older browser or while the above code is executing, this will result in three reorders.

Therefore, we can combine all the changes and deal with them in turn. 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
  • Example Modify the class of the CSS

    const el = document.getElementById('test');
    el.className += ' active';
    Copy the code

Modifying DOM in Batches

When we need to make a series of changes to the DOM, we can reduce the number of reflux redraws by following the steps:

  1. Detach elements from the document flow
  2. Modify it several times
  3. Brings 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 a backflow redraw because it is no longer in the render tree.

There are three ways to get the DOM out of the document stream:

  • Hide the element, apply the changes, and redisplay it
  • Use document fragments to build a subtree outside of the current DOM and copy it back into the document.
  • Copy the original element to a detached node, modify the node, and replace the original element.

Consider that we want to execute a piece of code to batch insert 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');
appendDataToElement(ul, data);
Copy the code

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

There are three ways to optimize:

Hide the element, apply the changes, and redisplay it

This will generate two backflows while showing and hiding the node

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

Use document fragments to build a subtree outside of 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

Copy the original element to a detached 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 all three cases, I wrote a demo to test before and after the changes on Safari and Chrome. However, the results are not very good.

Why: Why, as mentioned above, modern browsers use queues to store multiple changes for optimization, so we don’t really have to prioritize this optimization.

Avoid triggering synchronous layout events

As mentioned above, when we access some attributes of an element, it causes the browser to force an empty queue, forcing a synchronized layout. For example, if we want 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 may look fine, but it can cause significant performance problems. On each loop, an offsetWidth property of the box is read and used to update the width property of the p tag. This results in the browser having to make the style update from the previous loop take effect before responding to the style read from 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

Again, I wrote a demo to compare the performance of the two. You can check out the demo yourself. The performance gap is obvious.

For complex animation effects, use absolute positioning to take them out of the document stream

For complex animation effects, which often cause backflow redraws, we can use absolute positioning to take them out of the document stream. Doing so causes frequent backflow of the parent and subsequent elements. Let’s just do the last example.

After opening this example, we can open the console, which will output the current number of frames (although incorrect).

From the image above, we can see that the frame count never reached 60. At this point, if we click the button and set the element to absolute positioning, we can stabilize the frame to 60.

Css3 Hardware Acceleration (GPU Acceleration)

Rather than thinking about how to reduce backflow redrawing, we would rather not have backflow redrawing at all. This time, CSS3 hardware acceleration is coming!!

To highlight:

1. Use CSS3 hardware acceleration to ensure that transform, opacity, and filter animations do not cause backflow redraw.

2. Other animation properties, such as background-color, will still cause backflow redrawing, but it will improve the performance of these animations.

This article only discusses how to use it, not how it works, and will cover it in another opening article later.

How to use

Common CSS properties that trigger hardware acceleration:

  • transform
  • opacity
  • filters
  • Will-change

The effect

We can start with an example. I captured the reflux redraw over a period of time by using Chrome’s Performance. The actual result is as follows:

As you can see, there is no backflow redraw during the animation. If you’re interested, you can do your own experiment.

Focus on

  • With CSS3 hardware acceleration, transform, opacity, and filter animations do not cause backflow redraw
  • Other properties of the animation, such as background-color, will still cause backflow redrawing, but it will improve the performance of these animations.

Css3 hardware acceleration pits

Of course, everything good comes at a price, and too much of a good thing is too much of a good thing. Css3 hardware acceleration:

  • If you use CSS3 hardware acceleration for too many elements, it can lead to a high memory footprint and performance issues.
  • Rendering fonts on the GPU will invalidate anti-aliasing. This is because the GPU and CPU have different algorithms. So if you don’t turn off hardware acceleration at the end of the animation, the font will blur.

conclusion

This article mainly talks about the browser rendering process, the optimization mechanism of the browser and how to reduce or even avoid backflow and redraw, hoping to help you better understand the backflow redraw.

reference

  • Render trees are built, laid out and drawn

  • High-performance Javascript


“IVWEB Technology Weekly” shock online, pay attention to the public number: IVWEB community, weekly timing push quality articles.

  • Weekly articles collection: weekly
  • Team open source project: Feflow