目录
- 一、背景
- 二、动画卡顿具体分析
- 三、具体优化方法
-
- 3.1 JavaScript:优化 JavaScript 代码
-
- 1. requestAnimationFrame 优化
- 2. Web Worker
- 3.2 Style:减少 DOM 操作
- 3.3 Layout:避免频繁触发布局的动画
- 3.4 避免强制同步布局事件
- 3.5 Paint&Composite:GPU加速
一、背景
自 HTML 和 CSS 诞生以来,开发者在项目中经常使用动画来优化视觉效果,提升用户体验和留存,从远古时期 IE6 的各种滤镜到现在的 CSS3、canvas、SVG,愈发复杂的动画效果在即便是硬件性能快速增长的今天,性能问题一直是困扰我们的难题之一,本文旨在探讨影响动画性能的因素并寻找解决的办法。
动画卡顿可能是由多种原因引起的,下面罗列一些常见的分析和解决方法:
-
性能问题:动画卡顿通常是因为动画的渲染速度跟不上帧率的要求,导致画面不流畅。这可能是因为代码执行效率低下、大量 DOM 元素操作、复杂的 CSS 样式计算等原因导致的。解决方法包括
优化 JavaScript 代码
、减少 DOM 操作
、简化 CSS 样式
等。 -
内存泄漏:内存泄漏可能导致页面卡顿和性能下降。如果页面中有大量的动态生成的元素或者对象没有被正确地释放,会导致内存占用过高,从而影响页面性能。解决方法包括
及时释放不再需要的对象和资源
、避免创建过多的临时对象
等。 -
浏览器兼容性问题:不同浏览器对动画的处理方式和性能有所差异,某些浏览器可能不支持某些 CSS 或 JavaScript 特性,导致动画在特定浏览器中出现卡顿或者不流畅的情况。解决方法包括使用浏览器兼容性较好的特性、进行兼容性测试和调整等。
-
硬件加速:使用硬件加速可以提高动画的性能和流畅度,尤其是在移动设备上。可以通过 CSS 属性
transform
、opacity
、filter
等来触发硬件加速,从而提高动画的渲染速度和流畅度。 -
帧率控制:帧率过高可能会导致动画卡顿,特别是在性能较低的设备上。因为虽然高帧率可以提供更流畅的动画效果,但过高时可能会超出系统计算资源的处理能力,导致页面卡顿。可以通过调整动画的帧率来降低 CPU 和 GPU 的负载,从而提高动画的性能和流畅度。
综上所述,要解决动画卡顿问题,首先需要分析问题的根本原因,然后采取相应的优化和调整措施来提高动画的性能和流畅度。
二、动画卡顿具体分析
参考文档:https://jelly.jd.com/exp/detail?id=5dc38940b73b47015299a49c
大多数设备的刷新频率是 60 帧/秒,也就是 1 秒钟的动画是由 60 个画面连在一起生成的,所以一般要求浏览器对每一帧画面的渲染工作要在 16ms 内完成。当渲染时间超出 16ms 时,1 秒钟内少于 60 个画面生成,就会有不连贯、卡顿的感觉,影响用户体验。
一个页面帧在客户端的渲染分为以下几步:
- JavaScript。实现动画效果、DOM 操作等
- Style。确认每个 DOM 元素应用的 CSS 样式规则
- Layout。计算每个 DOM 元素最终在屏幕上的大小和位置
- Paint。在多个图层上绘制 DOM 元素的文字、颜色、边框和阴影等效果
- Composite。按照合理的顺序合并图层并显示在屏幕上
浏览器在实际渲染页面的时候需要经过一系列的映射,由 HTML 页面构建出来的 DOM 树到最终的图层,映射过程如下图所示:
Node -> RenderObject
。DOM 树的每一个 Node 都有一个对应的 RenderObject(一对一关系)RenderObject -> RenderLayer
。一个或多个 RenderObject 对应一个 RenderLayer(多对一),RenderLayer 用于保证元素之间的层级关系,一般来说位于同一位置且层级相同的元素处于同一个 RenderLayer。只有某些特殊的 RenderObject 会专门创建一个新的渲染层,其他的 RenderObject 与第一个拥有的 RenderLayer 的祖先元素共用一个,比如上图中的 p 元素和其子元素 div。
常见的生成 RenderLayer 的 RenderObject 拥有如下一种特征:
- 页面根元素
- 有CSS定位属性(relative, absolute, fixed, sticky)
- transparent不为1
- overflow不为visible
- 有CSS mask属性
- 有CSS box-reflect属性
- 有CSS filter属性
- 3D或硬件加速的2D canvas元素
- video元素
…
可以看出,具有上述特征的 RenderObject 会专门生产新的 RenderLayer,图层能够阻⽌该节点的渲染⾏为影响别的节点,即对页面的某些部分进行独立的处理,从而提升渲染性能,⽐如对于 video 标签来说,浏览器会⾃动将该节点变为图层。但 RenderLayer 过多就会影响页面的渲染速度,也会消耗内存资源,因此应该谨慎使用。
可以使用开发者工具来检查页面的图层信息,以便了解哪些部分被设置为图层,以及优化渲染性能。在 Chrome 浏览器中,在右上角更多工具找到图层
。
RenderLayer -> GraphicsLayer
。一个或多个 RenderLayer 对应一个 GraphicsLayer(多对一)。某些被认为是 Compositing Layer 的 RenderLayer 单独对应一个 GraphicsLayer,其他 RenderLayer将与第一个拥有 GraphicsLayer 的祖先元素共用同一个 GraphicsLayer。对于 GraphicsLayer 来说,每一个 GraphicsLayer 有一个 GraphicsContext 用于绘制其对应的 RenderLayers,合成器 Composite 将 GraphicsContexts 的位图(这个将由GPU实现)合成最终显示在屏幕上。
常见的 RenderLayer 会被提升为 Compositing Layer(单独用一个 GraphicsLayer) 的原因:
- 有3D transform属性。使用
translate3d()
,translateZ()
,scale3d()
,rotate3d()
等方法来对元素进行 3D 变换- 有perspective属性。设置元素的透视效果
- 3D canvas或硬件加速的2D canvas
- 硬件加速的iframe元素(如iframe嵌入的页面有合成层,合成层需要硬件加速)
- 使用了硬件加速的插件,如flash
- 对opacity/transform属性应用了animation/transition(当animation/transition为active)
- 子元素是compositing layer
- 兄弟元素是compositing layer,与当前的非composting layer有重叠,层级低于当前层
- 有will-change属性。明确告知浏览器该元素将会发生变化,可以预先进行优化处理
…
注意,提升为 Compositing Layer 可以提高页面的渲染速度。
三、具体优化方法
通过上面分析和学习,我们对优化方向已经有了一定着手点。
3.1 JavaScript:优化 JavaScript 代码
1. requestAnimationFrame 优化
关于 WHAT - requestAnimationFrame 介绍 中我们介绍过“对于动画效果,推荐使用 requestAnimationFrame 方法,它可以更有效地与浏览器的绘制周期同步,提供更流畅的动画效果,并且不会出现页面不可见时的执行问题”。
并且在 HOW - 前端定时器实践(含防抖、interval 模拟) 中我们也详细介绍过计时器存在的问题,计时器无法保证回调函数的执行时机,可能会在一帧内一并执行多次导致多次页面渲染,浪费 CPU 资源甚至产生卡顿,或者是在一帧即将结束时执行导致重新渲染,出现掉帧问题。
而使用 requestAnimationFrame:
- 更好的函数节流,其循环间隔是由屏幕刷新频率决定的,保证回调函数在屏幕的每一次刷新间隔中只执行一次
- 当页面被隐藏或最小化时,暂停渲染
优化效果比较:推荐在 https://codesandbox.io 调试
<button id="choice-1">setTimeout</button>
<button id="choice-2">requestAnimationFrame</button>
<button id="clear">clearAll</button>
<div id="result"></div>
// setTimeout 3次渲染
document.getElementById('choice-1').addEventListener('click', function () {
document.getElementById('result').innerHTML = '';
var i = 0;
while (i < 5000) {
var spanNode = document.createElement('span');
var txt = document.createTextNode('time');
spanNode.appendChild(txt);
document.getElementById('result').appendChild(spanNode);
i += 1;
}
setTimeout(function () {
var j = 0;
while (j < 200) {
console.log(j);
j += 1;
}
while (j > 0) {
var divNode = document.createElement('div');
var txt = document.createTextNode('1111111');
divNode.appendChild(txt);
document.getElementById('result').appendChild(divNode);
j -= 1;
}
setTimeout(function