Я оптимизировал панель прогресса и производительность страницы увеличилась на 70%

внешний интерфейс JavaScript
Я оптимизировал панель прогресса и производительность страницы увеличилась на 70%

предисловие

Всем привет, меня зовут Zero One. Недавно я собирался поговорить о коде в группе, поэтому перед проектом разобрался с бизнес-кодом. В процессе разбора я увиделкомпонент индикатора выполненияОн очень хорошо написан, что напоминает мне код прогресса, который я написал, когда впервые начал изучать интерфейс.По сравнению с этим он действительно слишком большой (большинство новичков не должны думать об этом, и моя первая стажировка компания принесла так мой наставник).

Так что я хочу поделиться этим со всеми вамиотличная идеяизкомпонент индикатора выполнения, он тоже существуеточень серьезныйпроблемы с производительностью, которые будут объяснены в конце этой статьипроблема заключается втак же какМетод оптимизации

Сценарии применения индикатора выполнения

Как правило, компоненты Progress Bar появляются в сценах, таких как Douyin играют в видео, как показано стрелкой в ​​нижней части рисунка:

image.png

Индикатор выполнения растет с увеличением длины видео, видео приостанавливается, а анимация индикатора выполнения также приостанавливается.

Далее давайте посмотрим, как это пишет большинство людей, и почему они говорят, что идея и исполнение не годятся. Взяв здесь в качестве примера React, разработчикам Vue не нужно бояться непонимания, главное думать

Основные функции реализации:

  • Поддержка воспроизведения, паузы, повтора
  • После окончания воспроизведения количество воспроизведений равно +1, и воспроизведение начинается заново.

Не рекомендуется написание

составная часть

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

let timer = null  //  递增进度的定时器
let totalTime = 3000  // 假设视频播放为3s

function App() {
    const [progress, setProgress] = useState(0)  // 进度
    const [isPlay, setIsPlay] = useState(false)  // 是否播放
    
    // setProgress的递增逻辑
    const handlerProgress = pre => {
        if(pre < 100) return pre + 1;
        else {  
          alert('播放结束')
          return 0   // 播放结束,重新开始播放
        }
    }
    
    // 开始播放 && 暂停播放
    const handleVideo = () => {
        setIsPlay(!isPlay)
        isPlay
        ? clearInterval(timer)
        : timer = setInterval(() => setProgress(handlerProgress), totalTime / 100)
    }
    
    // 重播
    const replay = () => {
        setIsPlay(true)
        if(timer) clearInterval(timer);
        setProgress(0)
        timer = setInterval(() => setProgress(handlerProgress), totalTime / 100)
    }
    
    return (
        <div id="root">
            <button onClick={handleVideo}>{ isPlay ? '暂停' : '播放' }</button>
            <button onClick={replay}>重播</button>
            <div className="container">
                <div className="progress" style={{ width: `${progress}%` }}/>
            </div>
        </div>
    )
}

раздел стиля

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

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

Давайте кратко продемонстрируем, как выглядит этот индикатор выполнения.

0opakkask9kklana669029.gif

Почему вы говорите, что это не лучший способ написать это? Поскольку мы используем таймер для быстрого увеличения переменнойprogressТаким образом, прогресс увеличивается, и каждое изменение переменной заставит представление пересчитывать рендеринг, что должно быть очень низкой производительностью (честно говоря, когда я испытывал эту демонстрацию, было небольшое зависание, видимое для просмотра). невооруженным глазом)

В добавок к этому? На самом деле, есть еще одна причина для причинения коробки, вы, возможно, пожелаете догадаться, давайте поместим его в последний разговор, я хочу знать маленького партнера, что ответ может скользнуть непосредственно к следующему

Рекомендуемое написание

То, что я рекомендую здесь, — это лучшее решение, которое я увидел, когда читал код, и я поделюсь им с вами в следующий раз.

составная часть

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

let totalTime = 3000  // 假设视频播放为3s

function App() {
    const [isPlay, setIsPlay] = useState(false)  // 是否播放
    const [count, setCount] = useState(0)  // 播放次数
    const [type, setType] = useState(0)   // 使用哪个动画。0: @keyframes play; 1: @keyframes replay;
    
    // 暂停 && 播放
    const handleVideo = () => setIsPlay(!isPlay);
    
    // 重播
    const replay = () => {
        setIsPlay(true)
        setType(type ? 0 : 1)
    }
    
    // 动画结束时触发的事件
    const end = () => {
        setCount(count + 1)  // 播放次数 +1
        replay()   // 重新开始播放
    }
    
    return (
        <div id="root">
            <button onClick={handleVideo}>{ isPlay ? '暂停' : '播放' }</button>
            <button onClick={replay}>重播</button>
            <span>{ `播放次数为:${count}` }</span>
            <div className="container">
                <div 
                    className={`progress ${isPlay ? 'play' : 'pause'}`} 
                    style={{
                        animationDuration: `${totalTime}ms`,
                        animationName: `${type ? 'replay' : 'play'}`
                    }}
                    onAnimationEnd={end}  // 动画结束时的事件
                />
            </div>
        </div>
    )
}

раздел стиля

@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 {     /* 使animation动画启动 */
    animation-play-state: running;
}

.progress.pause {    /* 使animation动画暂停 */
    animation-play-state: paused;
}

Мы установили два@keyframesАнимация заключается в том, чтобы сделать переключение при воспроизведении индикатора выполнения, то есть, когда вы нажимаете «повторить», вы можете напрямую переключаться на другую анимацию, а индикатор выполнения может увеличиваться с 0.

В то же время мы также устанавливаем стили двух имен классов, которые используются для управления воспроизведением и паузой анимации соответственно.

Когда воспроизведение завершено, через событие можно передать функцию числа воспроизведения +1.animationendконтролировать

Так же посмотрим на рендеры этой схемы (функция точно такая же, как и у предыдущей схемы)

0opakkask9kklana669029.gif

Сравнивая предыдущий набор решений, вы можете видеть, что этот метод написания не требует постоянного изменения данных для управления изменением представления, что сокращает количество вычислений в фреймворке и значительно повышает производительность.

дефект

Хотя производительность второго решения очень хорошая, как и у первого решения, есть еще одна скрытая проблема производительности, которая также обнаруживается, когда я исследую проблему производительности кода моего бывшего коллеги.

Недостаток: оба решения будут вызывать частые перекомпоновки и перерисовки.

можно использоватьchrome devtools performanceпроверить статус страницы

11010asdakkkakasd69029.gif

Небольшой индикатор выполнения вызывает так много перекомпоновок и перерисовок, так какой же эффект он имеет? Давайте кратко рассмотрим влияние перекомпоновки и перерисовки.

переставлять: Браузер должен пересчитать геометрию элемента, и это изменение также может повлиять на геометрию или положение других элементов.

перерисовать: Не все изменения DOM влияют на геометрические свойства элемента.Если изменение цвета фона элемента не влияет на его ширину и высоту, то в этом случае произойдет только перерисовка, а перекомпоновки не произойдет из-за компоновки элемента элемент не изменился

Поэтому, узнав о серьезных проблемах, вызванных перестановкой и перерисовкой, мы сразу же анализируем и оптимизируем ее.

Максимальная оптимизация

Сначала рассмотрим очень распространенную схему.image.png

Рендеринг страницы обычно следует этим пяти процессам. Конечно, есть также способы пропустить определенные промежуточные шаги, например, избежатьLayoutа такжеPaint

Давайте рассмотрим, какие методы приведут перегруппирование и перекраску.

Перемещение триггера: добавление или удаление видимых элементов DOM, изменение положения элемента, изменение размера элемента (в том числе: поля, отступы, граница, высота и т. д.), изменение содержимого (например: изменение текста или изображение заменяется другим изображением другого размера), изменение размера окна браузера, черезdisplay: noneскрыть узел DOM и т. д.

Факторы, вызывающие перекраску: Перестановка должна вызвать перерисовку (важно), пройтиvisibility: hiddenСкрыть узел DOM, изменить цвет фона элемента, изменить цвет шрифта и т. д.

Итак, где в коде, который мы написали ранее, триггеры переупорядочиваются и перерисовываются? При простом осмотре нетрудно обнаружить, что в обеих схемах постоянно меняются элементыwidthОдно изменение ширины элемента неизбежно вызовет перестановку и перерисовку, не говоря уже об ультрачастом изменении!

Решение: Включите ускорение GPU, избегайте процесса перестановки и перерисовки и продвигайте индикатор выполнения в один слой, то есть он не влияет на другие элементы.

Давайте оптимизируем только второе решение ~ Нам нужно только изменить его содержимое css (отметив его как изменение)

@keyframes play {     /* 通过transform来启用GPU加速,跳过重排重绘阶段 */
    0% {  
        transform: translateX(-50%) scaleX(0);  /* 用 scaleX 来代替 width */
    }

    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%;   /* 初始宽度为100%,因为我们要对其缩放 */
    background-color: red;
    will-change: transform;   /* 通过will-change告知浏览器提前做好优化准备 */
    animation-timing-function: linear;
}

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

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

Вот краткое объяснениеtranslateXа такжеscaleXнастройки стоимости. установить индикатор выполненияwidth: 100%, мы проходимscaleX(0.5)Увеличив его наполовину, вы обнаружите, что индикатор выполнения составляет половину длины контейнера и центрирован. В это время нам нужно пройтиtranslateX(-25%)переместите его влево в крайнее лево, почему это-25%Шерстяная ткань? Поскольку индикатор выполнения занимает половину контейнера и расположен по центру, это указывает на то, что левый и правый пробелы точно(100% - 50%) / 2 = 25%, так что нетрудно узнать, когда начальное состояниеscaleX(0)час,translateXценность-(100% - 0%) / 2 = -50%

После этого мы снова используемperformanceПроверь это

kasjdaskdj0022asd69029.gif

Хорошо видно, что количество перестановок и перерисовок страниц значительно сократилось, а остальные в основном самые элементарные перестановки и перерисовки страниц.

Некоторые люди хотят сказать, что я титульный участник, и тогда я покажу вам, насколько оптимизирована производительность.

Давайте запустим его с тем, который только что был оптимизирован.performance

image.png

Глядя на правую часть изображения, FPS в основном стабилен на55 ~ 70между

Давайте посмотрим на первое решение в начале статьи.performanceбеговые очки

image.png

Глядя на правую часть изображения, FPS в основном стабилен на32 ~ 50между

Хорошо видно, что колебания FPS до оптимизации очень серьезные, то есть он недостаточно стабилен, поэтому подвержен проблеме зависаний, при этом изменение FPS после оптимизации невелико, а общее изменение тренд относительно плоский, почти прямой.

На такой минималистичной странице наша оптимизированная производительность улучшилась примерно на40% ~ 54%

Тогда, если в обычном проекте, учитывая сложность страницы, наше оптимизированное решение не только избегает повторного расчета и рендеринга страницы, но и избегает перерисовки и перекомпоновки, то можно предположить, что в этом случае прирост производительности должен быть далек больше, чем40% ~ 54%Да, эммммммм, так что не будет преувеличением сказать, что производительность улучшилась на 70% ххххх

пасхальные яйца

Включение ускорения графического процессора переместит элементы в отдельный слой, что мы можем сделать с помощьюchrome devtools layersсмотреть

image.png

Вот слои страницы до и после оптимизации.

«До оптимизации»

image.png

Очевидно, что на всей странице есть толькоdocumentслой, то есть индикатор выполнения не многоуровневый

"Оптимизированный"

image.png

Также очевидно, что индикатор выполнения был выделен в отдельный слой.

конец

Прошлые рекомендации:

Я ноль один, если моя статья была вам полезна, пожалуйста, нажмитеотличный👍🏻 поддержите меня

Я создал публичный аккаунт: Front-end Impression, который каждый день выкладывает качественные статьи. Кроме того, вы также можете присоединиться к моей фронтенд-группе, поболтать и похвастаться технологиями, vx:Lpyexplore333