吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6797|回复: 58
收起左侧

[其他原创] 使用Chrome Performance进行性能分析(多个实战案例)

  [复制链接]
hans7 发表于 2022-11-7 01:33
本帖最后由 hans7 于 2022-11-9 01:51 编辑

引言

以前一直以为用Chrome Performance面板进行性能分析很难。实际做过以后方能认识到这是多么简单!

海量图片预警!

作者:hans774882968以及hans774882968以及hans774882968

本文52pojie:https://www.52pojie.cn/thread-1708906-1-1.html

重排分析

例1:入门

这一节主要是在复现参考链接1的内容。我们来写一个简单的demo:点击.div1,该元素高度变大。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>reflow</title>
  <style>
    .div1 {
      width: 100px;
      height: 100px;
      background-color: deepskyblue;
    }
    .div2 {
      width: 200px;
      height: 200px;
      background-color: pink;
    }
  </style>
</head>
<body>
  <div class="div1"></div>
  <div class="div2"></div>
  <script>
    function main() {
      const div1 = document.querySelector('.div1')

      function div1Click() {
        div1.style.height = '200px'
      }

      div1.addEventListener('click', div1Click)
    }
    main()
  </script>
</body>
</html>

我们打开Chrome Performance,点击箭头所示的按钮进行录制,并点击一下.div1,生成性能分析结果。

reflow-1-performance示意图.jpg

Main表示主线程,渲染和JS执行都是在主线程进行的。我们看到主线程有两个值得关注的Task,第一个Task主要依次Schedule Style Recalculation、点击事件、Recalculate StyleLayout组成,第二个Task主要依次PaintComposite Layers组成。这里一个Task表示一个事件循环,Layout事件表示重排,Paint事件表示重绘,重排和重绘可能在同一个Task也可能不在同一个Task执行。

看第一个Task:

reflow-2-查看点击事件.jpg

点击色块可以查看相关信息。

  • 箭头所指为Compile Code,耗时37 μs。
  • div1Click左侧有一个非常细的紫色块Schedule Style Recalculation
  • div1Click耗时0.11 ms。
  • click结束后,右侧有3个紫色块:第一个是Recalculate Style,耗时0.12 ms。
  • 第二个是Layout,耗时0.13 ms,Layout root#documentFirst Layout Invalidation是这行代码div1.style.height = '200px'。如下图所示。
  • 第三个是Pre-Paint,不关注。

reflow-3-Layout.jpg

第二个Task我们只看一下Paint

reflow-4-Paint.jpg

可以看到Layer Root#document,并且重绘耗时仅53μs。

例2:重排和重绘各发生几次?

我们基于例1的代码,稍微改下div1Click

      function div1Click() {
        div1.style.height = '200px'
        div2.style.height = '100px'
      }

请问它引起了几次重排?我们看一下Performance面板:

reflow-5-只发生一次重排.jpg

可以看到只引发了一次重排(Layout)和一次重绘(鼠标所指的Paint),并且发生在不同的事件循环。这大概是因为浏览器的优化!

接下来试试参考链接1所说的“一次事件循环(即一个Task)当中触发多次重排”的情况。再改下代码:

      function div1Click() {
        div1.style.height = '200px'
        console.log(div2.clientHeight)
        div2.style.height = '100px'
        console.log(div2.clientHeight)
      }

看下Performance面板:

reflow-6-2次重排 1次重绘.jpg

第一个Task的两个箭头是两次Layout,它们左边相邻的紫色块分别都是Schedule Style Recalculation(非常细)和Recalculate Style。第二个Task鼠标指向的是Paint。所以div1Click在第一个Task触发了2次重排,但仅在第二个Task触发了1次重绘。

我们点击两个Recalculate Style,发现两者都有一个之前没看到的属性Recalculation Forced,相关的代码分别是两句console.log(div2.clientHeight)。为了探究这个属性是否真的导致了强制重排,我们把代码改成:

      function div1Click() {
        div1.style.height = '200px'
        console.log(div2.clientHeight)
        div2.style.height = '100px'
      }

则发现重排仍发生2次,但第一次Recalculate StyleRecalculation Forced div1Click@reflow.html:29(即console.log(div2.clientHeight)),而第二次Recalculate Style没有。对比这些结果我们猜测,Recalculation Forced表示Performance面板分析出这次重排是被迫发生的,而发生的原因是我们读取了div2.clientHeight

接下来我们把修改div2的代码包裹进宏任务。

      function div1Click() {
        div1.style.height = '200px'
        console.log(div2.clientHeight)
        setTimeout(() => {
          div2.style.height = '100px'
          console.log(div2.clientHeight)
        })
      }

看下Performance面板,下面是刷新多次的结果:

图1

reflow-7-3个事件循环.jpg

图2

reflow-8-3个事件循环.jpg

图3

reflow-9-4个事件循环.jpg

观察到的一些现象:

  1. 重排肯定发生2次,但重绘可能发生1次也可能发生2次。如果重绘发生了2次,则Timer Fired的事件循环在重绘的事件循环之后。如果发生1次则Timer Fired的Task与点击的Task相邻。图1到图3重绘分别发生了1次、1次、2次。图1、图2的重绘Task和Timer FiredTask相隔长达几毫秒,所以截不进来。
  2. Schedule Style Recalculation是非常细的紫色块,既可能在函数执行的下面也可能在函数执行的左侧。图1到图3Schedule Style Recalculation分别在匿名函数的左侧、左侧、下面。3个图Schedule Style Recalculation都在div1Click下面仅仅是巧合。
  3. Install Timer是非常细的黄色块,既可能在setTimeout下面也可能在setTimeout左侧。图1到图3Install Timer分别在setTimeout下面、左侧、左侧。
  4. 如果发生了重排,则Schedule Style RecalculationRecalculate StyleLayout总是在同一个Task按时间顺序出现。

宏任务和微任务执行顺序分析

校招必考八股!来写一个简单的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>task</title>
</head>
<body>
  <script>
    function main() {
      console.log(1)
      setTimeout(() => {
        console.log(7)
      })
      console.log(2)

      function pro1() {
        function then1() {
          console.log(5)
        }

        new Promise((resolve) => {
          console.log(3)
          resolve()
        }).then(then1)
      }

      pro1()

      function pro2() {
        function then2() {
          console.log(6)
        }

        new Promise((resolve) => {
          setTimeout(() => console.log(8))
          resolve()
        }).then(then2)
      }

      pro2()

      function pro3() {
        new Promise((resolve) => {
          setTimeout(() => {
            console.log(9)
            resolve()
          })
        }).then(() => {
          console.log(10)
        })
      }

      pro3()
      setTimeout(() => {
        console.log(11)
      })
      console.log(4)
    }
    main()
  </script>
</body>
</html>

我们把Promise放进函数里,并写个then1then2,是期望在Performance里能方便地找到它们。如果不这么做,Performance里就不好找到它们(找不到的话就多刷新几次,看RP的)。

上面代码的输出为1~11。我们来简单分析一下:注意Promise传入的函数体也是同步任务,在执行resolve()的时候才会加入微任务队列。因此1~4执行后有2个微任务(5、6)和4个宏任务(7、8、9、11),先清空微任务再处理宏任务。执行9后将10加入微任务队列,因此10先于11执行。接下来看看Performance面板:

Task1:

microtask-1.jpg

点击查看的是then2,4个红色箭头指出的是Install Timer,绿色箭头指出的是pro2pro3

  1. 看到执行顺序是timer1 -> promise1 -> promise2 -> timer2 -> promise3 -> timer3 -> timer4,于是我们验证了:Promise传入的函数体是同步任务。
  2. 图右侧有一个Run Microtasks表示两个微任务then1then2执行了。1~6是在同一个事件循环中执行的。

后续Tasks:

microtask-2.jpg

可以看到,即使定时器设置的延迟时间为0,第一个事件循环和这4个事件循环也并没有挨在一起,而是由一个有重绘Paint的事件循环隔开了。这里4个定时器分为4个事件循环,各有一个粉色的(anonymous)块。其中,第三个宏任务在跑完同步任务(输出9)以后执行了一个微任务(输出10)。

综上:

  1. 不同的同步任务和不同的微任务可以在同一个事件循环中执行。
  2. 当前事件循环执行同步任务的过程中会加入一些宏任务和微任务。所有的微任务都会在当前事件循环的末尾执行完毕,而宏任务都要在后续的事件循环才能执行。
  3. 不同的宏任务在不同的事件循环中执行。
  4. 可以通过Performance面板来分析各种宏任务和微任务的真实执行顺序。

一个简单的寻找性能瓶颈的例子:Janky Animation

这一节主要是在复现参考链接2。案例:https://googlechrome.github.io/devtools-samples/jank/

为了方便,我们把代码copy到本地,再加一个显示当前图片总数的feature。完整代码就不贴出来了,太长了。

Janky_Animation-1.jpg

我们看到:

  1. 10个方块的时候,一个Task时间在3.5ms左右。
  2. 一个事件循环恰好有10个Schedule Style RecalculationRecalculate StyleLayout
  3. 和前面的案例不同,这里重排和重绘发生在同一个事件循环。

把方块个数增加到100:

Janky_Animation-2-100个.jpg

我们看到一个Task时间增加到了23~25ms。再看方块个数增加到150和200的情况:

Janky_Animation-3-150个.jpg

上图箭头指向的红条表示帧率过低。

Janky_Animation-4-200个.jpg

上图绿色箭头指向的红色三角标签表示耗时超过50ms的long task。

对于左下角的饼图,Idle的时间占多数才是正常的,比如10个方块的饼图:

Janky_Animation-5-10个图片时的饼图.jpg

对于200个方块的情况,我们查看某个方块的Layout,可以看到Performance已经贴心地帮我们指出了“Forced reflow(强制重排)是可能的性能瓶颈”。

Janky_Animation-6-强制重排.jpg

引起Recalculation Forced(在Recalculate Style事件)和Layout Forced(在Layout事件)的代码是同一行,点击查看:

Janky_Animation-7-读取offsetTop导致强制重排.jpg

于是我们找到了性能瓶颈:每个方块都要读取offsetTop,导致每个方块都引发了强制重排。

点击一下Optimize按钮,避免读取offsetTop

Janky_Animation-8-优化1:避免读取offsetTop.jpg

可以看到无论有多少方块,一个Task都只引发了一次重排,所以时间降为7ms左右,饼图也优化了许多:

Janky_Animation-9-优化1的饼图.jpg

但是查看代码可以看到,读写m.style的代码成为了新的性能瓶颈。

Janky_Animation-10-优化1的代码.jpg

这个需求仅仅是在展示运动的方块,因此我们可以用css3的transforms等属性,来达到类似的效果。另外,我们可以使用will-change属性,告知浏览器提前做好优化准备。

部分JS代码如下:

  app.update = function (timestamp) {
    for (var i = 0; i < app.count; i++) {
      var m = movers[i];
      if (app.optimize === 1) {
        var pos = m.classList.contains('down') ?
          m.offsetTop + distance : m.offsetTop - distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
        if (m.offsetTop === 0) {
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (m.offsetTop === maxHeight) {
          m.classList.remove('down');
          m.classList.add('up');
        }
      } else if (app.optimize === 2) {
        var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
        m.classList.contains('down') ? pos += distance : pos -= distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
        if (pos === 0) {
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (pos === maxHeight) {
          m.classList.remove('down');
          m.classList.add('up');
        }
      } else if (app.optimize === 3) {
        var pos = parseInt(
          m.style.transform.slice(
            m.style.transform.indexOf('(') + 1,
            m.style.transform.indexOf('px')
          )
        ) || 0;
        m.classList.contains('down') ? pos += distance : pos -= distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.transform = `translateY(${pos}px)`;
        if (pos === 0) {
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (pos === maxHeight) {
          m.classList.remove('down');
          m.classList.add('up');
        }
      }
    }
    frame = window.requestAnimationFrame(app.update);
  }

新增的CSS:

.proto.mover {
  will-change: transform;
}

这个方法有个美中不足之处:无法去除m.style.top(去除了优化就白做了),导致方块运动范围与前两种方案不一致。

Janky_Animation-11-优化2.jpg

此时我们看到只有Schedule Style RecalculationRecalculate Style,没有Layout事件,说明没有引起重排。一个Task的时间进一步降低为5ms左右。

再看看优化2的饼图,也比优化1更好:

Janky_Animation-12-优化2的饼图.jpg

另外,使用will-change的元素会单独分出一个图层,我们可以用Layers面板查看。不使用will-change

Janky_Animation-13-无单独图层.jpg

使用will-change

Janky_Animation-14-有单独图层.jpg

web worker案例

这一节主要是在复现参考链接4。

我们准备一段会阻塞渲染的代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>web worker</title>
</head>
<body>
<p id="p1"></p>
<p id="p2"></p>
<script>
  function main() {
    let ans1 = 0n
    for (let i = 1n; i <= 10000000n; i++) {
      ans1 += i * i
    }
    const p1 = document.getElementById('p1'), p2 = document.getElementById('p2')
    p1.innerText = ans1.toString()
    let ans2 = 0n
    for (let i = 1n; i <= 10000000n; i++) {
      ans2 += i * i * i
    }
    p2.innerText = ans2.toString()
  }
  main()
</script>
</body>
</html>

Chrome打开无痕窗口,消除插件的影响。点击Performance面板的Start profiling and reload page按钮,得下图:

web_worker-1-LCP.jpg

LCP大约3s,说明耗时计算的确阻塞了渲染。再放大看看f1f2的分界点:

web_worker-2-f1和f2运行分界点.jpg

看来f1常数比f2小些,是符合直觉的。当然更好的方式是直接在Sources面板看哪些代码执行时间最长。

web_worker-3-代码执行时间.jpg

我们有可能优化这个页面的LCP嘛?如果计算任务可拆分,那么我们可以参考React Fiber的架构,把任务拆开,组织成链表。但是这两个简单任务不能再拆分。怎么办?在参考链接4了解到Worker可以开启额外线程来运行耗时任务,达到不阻塞渲染任务的目的。于是我们可以接着写代码:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>web worker优化后</title>
</head>
<body>
<p id="p1">答案1</p>
<p id="p2">答案2</p>
<script>
  function main() {
    function runWorker(url, message) {
      return new Promise((resolve, reject) => {
        const worker = new Worker(url)
        worker.postMessage(message)
        worker.addEventListener('message', (e) => {
          resolve(e.data)
        })
        worker.onerror = reject
      })
    }

    const p1 = document.getElementById('p1'), p2 = document.getElementById('p2')

    async function f1() {
      const ans = await runWorker('./web_worker1.js', 10000000n)
      p1.innerText = ans.toString()
    }

    async function f2() {
      const ans = await runWorker('./web_worker2.js', 10000000n)
      p2.innerText = ans.toString()
    }

    f1()
    f2()
  }
  main()
</script>
</body>
</html>

web_worker1.js

addEventListener('message', (e) => {
  let ans = 0n;
  let num = e.data;
  for (let i = 1n; i <= num; i++) {
    ans += i * i
  }
  postMessage(ans);
});

web_worker2.js

addEventListener('message', (e) => {
  let ans = 0n;
  let num = e.data;
  for (let i = 1n; i <= num; i++) {
    ans += i * i * i
  }
  postMessage(ans);
});

注意点:

  1. Worker的第一个参数只能是url,这逼迫我们把耗时任务放到单个文件中。
  2. 对于Chromium内核的浏览器,需要开启http服务器,再打开index.html才能运行。否则会报错Failed to construct 'Worker': Script at '' cannot be accessed from origin 'null'.

Performance如下:

web_worker-4-优化后的Performance面板.jpg

我们看到LCP变成了60ms,耗时任务额外开了两个线程去运行,不再影响页面交互。它们的事件可以展开上图的两个Worker来查看。

进度条组件性能瓶颈分析(React Hooks CDN)

这一节主要是在复现参考链接3。现在要求你用React写一个进度条:

  • 支持播放、暂停、重播。
  • 播放结束后,播放次数+1,并重新开始播放。
  • 暂时不需要支持进度条拖拽等功能,纯展示。

实际上:不需要支持拖拽等功能时,用html5的progress标签最好;需要支持拖拽等功能时,只有做法1可行。但我们先忽略这些吧!

用CDN运行React Hooks代码

先介绍一下怎么用CDN跑React Hooks。

1、我们需要导入reactreact-domBabel

  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.0.0-beta.3/babel.min.js"></script>

2、我们需要一个挂载点div[id='app'],对应的挂载语句:ReactDOM.render(React.createElement(App), document.querySelector('#app'))

3、我们在脚手架开发时使用的ES6 Module写法,需要改成const {useState} = window.React

4、script标签需要声明为<script type="text/babel">

做法1:setTimeout

按照常人思维,我们大概率会这么写:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>progress组件</title>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.0.0-beta.3/babel.min.js"></script>
  <style>
    .container {
      height: 10px;
      border-radius: 5px;
      border: 1px solid black;
    }

    .progress {
      height: 100%;
      width: 0;
      border-radius: 5px; /* 与.container一致 */
      background-color: red;
    }
  </style>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
  function main() {
    const {useState} = window.React
    let timer = null  //  递增进度的定时器
    let totalTime = 3000  // 假设视频播放为3s

    function App() {
      const [count, setCount] = useState(0)
      const [progress, setProgress] = useState(0)  // 进度
      const [isPlay, setIsPlay] = useState(false)  // 是否播放

      const handlerProgress = pre => {
        if (pre < 100) return pre + 1;
        else {
          // 使用setCount(count + 1)则无法及时更新
          setCount(count => count + 1)
          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>
          <span>{`播放次数为:${count}`}</span>
          <div className="container">
            <div className="progress" style={{width: `${progress}%`}}/>
          </div>
        </div>
      )
    }

    ReactDOM.render(React.createElement(App), document.querySelector('#app'));
  }
  main()
</script>
</body>
</html>

Performance如下:

progress-1-普通做法的Performance.jpg

可以看到,每次更新进度条都需要一个宏任务,并且要触发一次重排。

做法2:构造@keyframes切换

接下来看一种比较精妙的做法(来自参考链接3)。这种做法自己想不到的,学一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>progress组件-v2</title>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.0.0-beta.3/babel.min.js"></script>
  <style>
    .container {
      height: 10px;
      border-radius: 5px;
      border: 1px solid black;
    }

    .progress {
      height: 100%;
      width: 0;
      border-radius: 5px; /* 与.container一致 */
      background-color: red;
      animation-timing-function: linear;
    }

    .progress.play {     /* 使animation动画启动 */
      animation-play-state: running;
    }
    .progress.pause {    /* 使animation动画暂停 */
      animation-play-state: paused;
    }
    @keyframes animeSwitch0 {
      to {
        width: 100%;
      }
    }
    @keyframes animeSwitch1 {
      to {
        width: 100%;
      }
    }
  </style>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
  function main() {
    const {useState} = window.React
    let totalTime = 3000  // 假设视频播放为3s

    function App() {
      const [count, setCount] = useState(0)
      const [animeSwitch, setAnimeSwitch] = useState(false)
      const [isPlay, setIsPlay] = useState(false)  // 是否播放

      // 开始播放 && 暂停播放
      const handleVideo = () => {
        setIsPlay(!isPlay)
      }

      const playEnd = () => {
        setCount(count + 1)
        replay()
      }

      // 重播
      const replay = () => {
        setIsPlay(true)
        setAnimeSwitch(!animeSwitch)
      }

      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: `animeSwitch${animeSwitch + 0}`
              }}
              onAnimationEnd={playEnd}
            />
          </div>
        </div>
      )
    }

    ReactDOM.render(React.createElement(App), document.querySelector('#app'));
  }
  main()
</script>
</body>
</html>

因为这个进度条组件是纯展示组件,所以可以用@keyframes来完成所有的要求。关键的思路是:我们有两份完全一样的@keyframes,通过切换animation-name来实现重播功能。播放结束时重新播放的要求也可以视为一次重播来实现,具体实现是监听了onAnimationEnd事件。Performance如下:

progress-2-animation法的Performance.jpg

这个做法依旧会频繁触发重排,因为@keyframes改变了元素的宽度。这种做法会导致Performance面板无法找到重排发生的原因。另外,性能的瓶颈比较突出:playEnd()

做法2的优化:使用translateX + scaleX来造成宽度改变的视觉效果

这个做法有一个更为精妙的优化,只需要修改做法2的CSS部分:

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

    .progress {
      height: 100%;
      width: 100%;
      border-radius: 5px; /* 与.container一致 */
      background-color: red;
      animation-timing-function: linear;
      will-change: transform;
    }

    .progress.play {     /* 使animation动画启动 */
      animation-play-state: running;
    }
    .progress.pause {    /* 使animation动画暂停 */
      animation-play-state: paused;
    }
    @keyframes animeSwitch0 {
      0% {
        transform: translateX(-50%) scaleX(0);
      }

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

      to {
        transform: translateX(0) scaleX(1);
      }
    }

在做法2的基础上,因为只需要进行展示,所以我们用translateX + scaleX代替了宽度的变化(视觉效果一样即可)。设当前缩放属性为scaleX(x),那么我们需要右移:- (100% - x) / 2,才能让起点始终左对齐。代入0100%,分别得-50%, 0。这就确定了@keyframes的写法。显然这种写法不会引起重排。另外,因为这个方案只有视觉效果,所以我们不妨用will-change: transform把进度条提升到一个单独的图层。

做法3的Performance图懒得截了,可以看下文“性能评估”一节。做法2和做法3的图层对比:

progress-3-v2无gpu加速.jpg

做法3:

progress-4-v3有gpu加速.jpg

进度条的确提升到了单独的图层。

性能评估

注:空闲时间的评估结论看上去不太对劲,仅供参考。

我们分两种情况:进度跨过了100%和进度没跨过100%,只录制2s左右。选择在第4次播放时开始录制。

进度没跨过100%:做法2的空闲时间 = 2004/2084,做法1的空闲时间 = 2045/2144,差为0.008,单位ms。差不多。

进度跨过100%:令我感到意外,做法2比做法1的性能差。不过做法3性能最好,还是符合直觉的。

做法1:

progress-5-v1性能.jpg

做法2:

progress-6-v2性能.jpg

做法3:

progress-7-v3性能.jpg

上图验证了我们的说法,做法3用translateX + scaleX代替做法2宽度的变化后,不会引起重排了。

总结

Chrome Devtools 的 Performance 工具是网页性能分析的利器,它可以记录一段时间内的代码执行情况,比如 Main 线程的 Event Loop、每个 Event loop 的 Task,每个 Task 的调用栈,每个函数的耗时等,还可以定位到 Sources 中的源码位置。

性能优化的目标就是找到 Task 中的 long task,然后消除它。因为网页的渲染是一个宏任务,和 JS 的宏任务在同一个 Event Loop 中,是相互阻塞的。

参考资料

  1. https://www.bilibili.com/video/BV1Pr4y1N7QZ
  2. 你不知道的chrome performance调试技巧:https://juejin.cn/post/6977637532494200863
  3. 我优化了进度条,页面性能竟提高了70%:https://juejin.cn/post/69768100169300050294. Chrome Performance + web worker:https://juejin.cn/post/7046805217668497445

免费评分

参与人数 27威望 +2 吾爱币 +127 热心值 +26 收起 理由
君同青空 + 1 + 1 谢谢@Thanks!
5omggx + 1 + 1 用心讨论,共获提升!
salt958 + 1 + 1 我很赞同!
xinin0909 + 1 + 1 用心讨论,共获提升!
LoveyLoverson + 1 + 1 谢谢@Thanks!
sqk950143960 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
zach14c + 1 谢谢@Thanks!
gdlwolf + 1 + 1 谢谢@Thanks!
qvsijia + 1 + 1 我很赞同!
阿斯顿 + 1 我很赞同!
吾之名翎 + 1 + 1 用心讨论,共获提升!
gaosld + 1 + 1 热心回复!
huaho + 1 + 1 我很赞同!
Fanghan + 1 + 1 谢谢@Thanks!
Fiverya + 1 + 1 谢谢@Thanks!
yixi + 1 + 1 谢谢@Thanks!
lcg2014 + 1 + 1 用心讨论,共获提升!
努力加载中 + 1 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
soenluzy + 1 + 1 我很赞同!
小菜鸟一枚 + 1 + 1 用心讨论,共获提升!
debug_cat + 1 + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
zhczf + 1 + 1 我很赞同!
wushaominkk + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
vethenc + 2 + 1 谢谢@Thanks!
为之奈何? + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| hans7 发表于 2022-11-9 01:53
今天更新了一个web worker的简单案例和一个React Hooks CDN的有一定难度的案例~
 楼主| hans7 发表于 2022-11-13 17:18
longbt 发表于 2022-11-12 23:37
这是开发测试必备技能啊

但是就这么一个必备技能,q内想找个通俗、详细+有实战案例的教程却还是难于登天
yonghu99999 发表于 2022-11-7 07:03
hnwang 发表于 2022-11-7 08:20
收藏了学习一下,感谢分享
vethenc 发表于 2022-11-7 08:29
技术大佬,真的试图教会我
SVIP008 发表于 2022-11-7 09:57
感觉看不懂,但要支持一下!
8970665 发表于 2022-11-7 13:09
是否看不懂的都是好东西!
wantwill 发表于 2022-11-7 16:11

学习了,虽然我也看不懂
wanghaowei666 发表于 2022-11-7 16:45
学习了,虽然我也看不懂
debug_cat 发表于 2022-11-7 17:26
后面可能对自己的网站进行优化,这个工具使用很棒
浔無涯 发表于 2022-11-8 09:05
非常有用, 之前也以为很难
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-19 20:14

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表