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