随着网络日益发达,网页的内容也更加丰富,形式也更加多样化。而随之而来的性能问题也不容小觑。这篇文章我会根据我在实践中遇到的一个问题来总结,我在面对性能问题的一些解决步骤,希望能对大家有所启发。
查找问题原因
我接触的项目中有数据可视化的功能,在编辑可视化组件时页面会有明显卡顿,并且在页面加载完毕后也能感受到操作不流畅。
这种情况下我们一般使用排除法来查找出现问题的原因。首先查看 Devtool 是否有报错的情况,其次检查网络连接状况。
我使用的浏览器是 Chrome,如果有报错就会在 Console 里面观察到。网络状况我一方面使用 Ping 来测试服务器连接是否异常,另一方面选择下载文件(我下载的是 node.js)来测试网络速度,以此来判断是否网络问题造成的卡顿。
在我完成了对上面两点的检测后我就排除了由代码或网络引发这个问题的可能性。那么接下来很容易就让人想到是否性能出现了不合理的消耗。
关于性能,我们都知道网页所消耗的计算机资源第一是内存,第二是CPU 。我用的系统是 Windows,而这两个指标可以在 Task Manager 中查看。
上图是相对正常时各指标的占用率或速率。当时我所观察到的情况是,内存保持在 50%-60% 的占用率,而 CPU 的占用率已经超过 95%,甚至到达 100%。那么原因就一目了然了,一定是出现了大量的需要计算的情况才导致页面卡顿。再考虑到页面在加载完成后依然保持极高的 CPU 占用率。
由此推断,最有可能的情况是:当前页面存在一个或多个不间断的监听事件,这些监听事件随着页面被加载而运行,导致 CPU 资源被持续占用。
解决过程
既然已经得出了一个合理的假设,那么就可以查看代码中是否存在持续调用的监听事件了。实际上,我在项目中发现了多处监听。
window.resize,mousemove,scroll,还包括钩子函数。这些都是在满足条件时会持续调用的方法,那么怎么确定是哪一个出现了问题呢?
我第一次判断是 window.resize 的问题,因为我在 window.resize 的回调函数中看到了一些 DOM 操作。这里其实应该做进一步的验证,但是我想当然的下了结论。我将 window.resize 删掉并测试结果,发现 CPU 占用率确实降低了,但是闲置时依然高达 60%。正常情况下这个数值应当不超过 10%。我意识到我的结论下的太早了,我需要更多的信息来找出真正的原因。
这时我想到可以借助工具,比如 Devtools。
使用 Chrome 的 Performance 来对页面的性能进行分析,结果如下:
这是从打开可视化编辑页面直到页面完全加载完毕的性能分析报告。从节选出的这部分可以看到一个叫 Animation frame 的 function 在持续的执行,问题肯定就出在它身上。
再次对代码进行查看后我将目标锁定在了 window.requestAnimationFrame() 上。
requestAnimationFrame
先来简单介绍 requestAnimationFrame:
它是一个浏览器的宏任务。 requestAnimationFrame 的用法与 setTimeout、setInterval 很相似,只是不需要设置时间间隔而已。requestAnimationFrame 使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给 cancelAnimationFrame 用于取消这个函数的执行。
大多数电脑显示器的刷新频率是 60Hz,大概相当于每秒钟重绘 60 次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此最平滑动画的最佳循环间隔是1000ms/60,约等于 16.6ms。
在之前,为了实现动画,常常使用 setTimeout() 或 setInterval() 方法来定时更新动画帧。然而,这种方法并不理想,因为它们受限于定时器的精度和浏览器在不同设备上的性能差异。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
requestAnimationFrame 的出现解决了这个问题。它使用浏览器的刷新率作为参考,确保动画帧的更新在每一帧之间的间隔是最佳的,从而实现更加流畅和自然的动画效果。
requestAnimationFrame 引发的问题
如上所述,requestAnimationFrame 是一个类似于 setTimeout 或 setInterval 的方法。它虽然对性能更加友好,但是使用不当也会出现我们现在看到的问题。原因主要集中在 requestAnimationFrame 的回调函数。
requestAnimationFrame 作为一种高频发的事件,假如在回调函数中频繁的进行 DOM 操作,并且没有及时停止事件的触发,就会对性能造成巨大的影响。
项目中用到 requestAnimationFrame 的地方有两个,其中一个是 window.resize 另一个是 scroll。这也就是为什么我第一次尝试删掉 window.resize 的时候 CPU 占用率只下降到 60% 的原因。
接下来就是动手解决这个问题了。我发现 window.resize 中的 DOM 操作是为了给一个变量赋值,而这个变量在 scroll 的回调中被用到,而其它的代码已经失去了实际的作用。所以将两处代码整合,放弃使用 requestAnimationFrame,直接监听 scroll event。那么 requestAnimationFrame 也就可以删掉了。
做完这一切之后,再检测 Chrome 的 Performance 结果如下:
可以看到资源的消耗主要集中在加载页面的期间,而在闲置阶段不会过多的占用性能。
解决问题的方法不止一个,我之所以能直接删掉 requestAnimationFrame,而不担心破坏原有功能是因为项目中的这段代码已经没有实际作用了,属于历史遗留问题。
而如果我们要保证原有功能的正常运行,也有两个方案可以选择。
- 替代:requestAnimationFrame 往往被用于动画渲染方面的功能。尝试用 CSS 动画来代替它
- 优化:不在 requestAnimationFrame 的回调中做大量运算,在满足条件后及时的停止 requestAnimationFrame
总结
性能问题在实际项目中已经屡见不鲜,我们想改善项目的体验或者降低资源的消耗就必须要面对它。相对于常见的 bug 或者新增功能来说,解决性能问题更加的棘手。另外,在没有直接证据的情况下,不应该凭直觉来判断原因,而是多利用各种工具来解决问题。
为了方便入手解决以后可能遇到的类似问题,我将过程总结如下:
以上是我对此次资源占用问题的全部总结,谢谢。