preface

Hello, I’m one one. Recently I was going to talk about code in the group, so I went through the business code before the project. In the process of sorting out, I saw that there was a progress bar component that was very well written, which reminded me of the progress bar code I wrote when I first started learning the front end, which was really far away from this one (most beginners should not have thought of it, and my mentor was the same at my first internship company).

So, I want to share with you the progress bar component that is a great idea, but it also has a very serious performance problem, which I’ll explain at the end of this article and how to optimize

Application scenarios of the progress bar

Generally, progress bar components appear in a scenario like douyin play video, as shown by the arrow at the bottom of the picture:

The progress bar grows with the length of the video, and when the video is paused, the animation of the progress bar is paused

Let’s take a look at how most people write and why it’s not good thinking and performance. Take React as an example here. Vue developers need not be afraid of not understanding it, but mainly looking at the idea

Main functions:

  • Supports play, pause, and replay
  • After the playback ends, add 1 to the number of playback times and start the playback again

Not recommended

Component part

// index.jsx
import { useState } from 'react'
import './index.css'

let timer = null  // The timer for incrementing the progress
let totalTime = 3000  // Assume that the video playback is 3s

function App() {
    const [progress, setProgress] = useState(0)  / / schedule
    const [isPlay, setIsPlay] = useState(false)  // Whether to play
    
    // setProgress increments logic
    const handlerProgress = pre= > {
        if(pre < 100) return pre + 1;
        else {  
          alert('Play over')
          return 0   // Start again}}// Start playing && Pause playing
    const handleVideo = () = >{ setIsPlay(! isPlay) isPlay ?clearInterval(timer)
        : timer = setInterval(() = > setProgress(handlerProgress), totalTime / 100)}/ / replay
    const replay = () = > {
        setIsPlay(true)
        if(timer) clearInterval(timer);
        setProgress(0)
        timer = setInterval(() = > setProgress(handlerProgress), totalTime / 100)}return (
        <div id="root">
            <button onClick={handleVideo}>{ isPlay ? 'Pause' : 'play'}</button>
            <button onClick={replay}>The replay</button>
            <div className="container">
                <div className="progress" style={{ width:` ${progress`}}} % / >
            </div>
        </div>)}Copy the code

Style part

.container {
    height: 10px;
    border-radius: 5px;
    border: 1px solid black;
}

.progress {
    height: 100%;
    width: 0;
    background-color: red;
}
Copy the code

Here’s a quick demonstration of what this progress bar looks like

Why is this not a good way to write it? Since we are increasing the progress by rapidly incrementing the variable progress with a timer, each change in the variable drives the view to recalculate the render, which is bound to be very poor performance (to be honest, I saw a little lag when I was playing the demo).

And beyond that? There’s another reason why it’s stuck, and if you can guess, we’ll talk about it at the end, and if you want to know, you can slide down here

Recommended way to write

Here is the recommended is I read the code when the better solution, I will share with you

Component part

// index.jsx
import { useState } from 'react'
import './index.css'

let totalTime = 3000  // Assume that the video playback is 3s

function App() {
    const [isPlay, setIsPlay] = useState(false)  // Whether to play
    const [count, setCount] = useState(0)  // Play times
    const [type, setType] = useState(0)   // Which animation to use. 0: @keyframes play; 1: @keyframes replay;
    
    // Pause && play
    const handleVideo = () = >setIsPlay(! isPlay);/ / replay
    const replay = () = > {
        setIsPlay(true)
        setType(type ? 0 : 1)}// The event that is triggered when the animation ends
    const end = () = > {
        setCount(count + 1)  // Play times +1
        replay()   // Start the play again
    }
    
    return (
        <div id="root">
            <button onClick={handleVideo}>{ isPlay ? 'Pause' : 'play'}</button>
            <button onClick={replay}>The replay</button>
            <span>${count} '}</span>
            <div className="container">
                <div 
                    className={`progressThe ${isPlay ? 'play' : 'pause` '}}style={{
                        animationDuration:` ${totalTime}ms`,
                        animationName:` ${type ? 'replay' : 'play` '}}}onAnimationEnd={end}// The event when the animation ends />
            </div>
        </div>)}Copy the code

Style part

@keyframes play {   
    to {
        width: 100%; }}@keyframes replay {
    to {
        width: 100%; }}.container {
    height: 10px;
    border-radius: 5px;
    border: 1px solid black;
}

.progress {
    height: 100%;
    width: 0;
    background-color: red;
    animation-timing-function: linear;
}

.progress.play {     /* Start the animation */
    animation-play-state: running;
}

.progress.pause {    /* Pause the animation */
    animation-play-state: paused;
}
Copy the code

We set up the two @KeyFrames animations so that we can make a switch when we want the progress bar to play again, that is, when we hit “Replay”, we can switch directly to another animation, so that the progress bar increments from 0

We also style two class names to control the play and pause of the animation

When the playback is complete, the +1 number of playback can be monitored by the event AnimationEnd

Again, take a look at the renderings of this scheme (which does exactly the same thing)

Compared to the previous approach, you can see that this approach eliminates the need to constantly modify the data to drive view changes, reducing the amount of computation in the framework and improving performance

defects

The second solution has good performance, but just like the first solution, there is another hidden performance problem, which I found when I checked the performance problems of my former colleague’s code.

Pitfalls: Both schemes cause frequent rearrangements and redraws

You can use Chrome DevTools Performance to verify the condition of the page

A small progress bar triggers so many rearrangements and redraws, so what effect does it have? Let’s briefly review the effects of rearrangements and redraws

Reorder: The browser needs to recalculate the geometric attributes of the element, and the geometric attributes or positions of other elements may be affected by the change.

Redraw: Not all DOM changes affect the geometry of an element. Changing the background color of an element does not affect its width or height. In this case, only one redraw will occur, and no rearrangement will occur, because the layout of the element has not changed

So knowing the serious problems caused by rearrangement and redrawing, we immediately analyzed and optimized it

Extreme optimization

Let’s start with a very common diagram

Page rendering, in general, goes through these five processes. Of course, there are ways to skip some intermediate steps, such as avoiding Layout and Paint

Let’s review some of the ways in which rearrangements and redraws can occur

Factors that trigger rearrangement: adding or removing visible DOM elements, changing the position of elements, element size changes (margin, margin, border, height, etc.), content changes (e.g. text changes or an image is replaced by another image of a different size), browser window size changes, using display: None hides a DOM node, etc

Factors that trigger redrawing: rearrangement must trigger redrawing (important), use visibility: hidden to hide a DOM node, modify the element background color, modify the font color, and so on

So where exactly in the code we wrote earlier triggered the rearrangement and redrawing? If the width of the element is changed, it will be rearranged and redrawn. If the width of the element is changed, it will be rearranged and redrawn.

Solution: Enable GPU acceleration to avoid rearranging and redrawing and elevate the progress bar to a separate layer without affecting other elements

For the second solution, we only need to change the CSS content.

@keyframes play {     /* Transform to enable GPU acceleration, skip the reorder redraw phase */
    0% {  
        transform: translateX(-50%) scaleX(0);  /* scaleX */
    }

    to {
        transform: translateX(0) scaleX(1); }}@keyframes replay {
    0% {
        transform: translateX(-50%) scaleX(0);
    }

    to {
        transform: translateX(0) scaleX(1); }}.container {
    height: 10px;
    border-radius: 5px;
    border: 1px solid black;
}

.progress {
    height: 100%;
    width: 100%;   /* The initial width is 100%, because we want to scale it */
    background-color: red;
    will-change: transform;   /* Use will-change to tell the browser to prepare for optimization */
    animation-timing-function: linear;
}

.progress.play {    
    animation-play-state: running;
}

.progress.pause {   
    animation-play-state: paused;
}
Copy the code

Here’s a quick explanation of the numeric Settings for translateX and scaleX. Set width: 100% for the progress bar and scale it by half using scaleX(0.5). It can be found that the progress bar is half the length of the container and is in the center. At this point, we need to translate it left to the left by translateX(-25%). Since the progress bar takes up half of the container and is centered, it means that the left and right white space is exactly (100%-50%) / 2 = 25%, so it is not difficult to know that when the initial state scaleX(0), translateX is -(100%-0%) / 2 = -50%

Having done that, let’s check again with performance

You can clearly see that the number of rearranges and redraws of the page has been reduced by many, many, many times, and the rest is basically the most basic rearranges and redraws of the page.

Some people are going to say I’m a clickbait, so I’m going to show you how much performance has been optimized

First, run performance with the ultimate optimization

On the right, the FPS is basically stable between 55 and 70

Take a look at the performance score of the first scenario at the beginning of the article

On the right, the FPS is basically stable between 32 and 50

It is clear that the FPS before optimization fluctuates a lot, which means it is not stable enough, so it is prone to stalling. In the optimized FPS, the change is not much, and the overall change trend is relatively flat, almost a straight line

On such a minimalist page, we’ve all improved performance by about 40-54%

So if in a normal project, given the complexity of the page, we optimized the solution to avoid the repeated rendering of the page and avoid the backflow of redrawing, the performance improvement in that case would be much more than 40% ~ 54%, emmmmmm, so I would say 70% performance improvement is not too much HHHHH

Small eggs

Enabling GPU acceleration will elevate elements to a separate layer, which can be seen by Chrome Devtools Layers

Here we show the page layer before and after optimization

“Before optimization”

Obviously, the entire page has only the Document layer, meaning the progress bar is not layered out

“Optimized”

It’s also obvious that the progress bar is on a separate layer

At the end

Previous recommendations:

  • A handy Markdown editor for 10 minutes (159+ 👍🏻)
  • I learned a lot from last night’s live telecast!! (517 + 👍 🏻)
  • Learn how to troubleshoot memory leaks (1029+ 👍🏻)

I am one one, if my article is helpful to you, please support me by clicking 👍🏻

I made an official account: front impression, push quality articles every day. You are also welcome to join my front end group to chat and brag about communication techniques, VX: Lpyexplore333