一、你以为优化动画只改CSS和JS就够了?
做网页动画的人,大多会把精力放在两个地方:要么改CSS里的动画属性,要么优化JS里的动画逻辑。比如有人会把transition改成transform加opacity,觉得这样就不会触发重排重绘;还有人会把JS里的定时器改成requestAnimationFrame,以为帧率就能稳下来。但很多时候改完之后,动画还是卡——这时候你大概率没注意到HTML的结构,还有浏览器悄悄给你加的合成层。
先给你说个我踩过的坑:之前做过一个电商首页的商品列表动画,鼠标悬停的时候,商品卡片会放大、加阴影,同时顶部的标签会滑出来。我一开始觉得动画用了transform和opacity,肯定没问题,结果测的时候,鼠标快速滑过卡片,帧率直接掉到30以下,手机上更卡。我查了半天CSS和JS,都没发现问题,最后才发现是HTML结构和合成层的锅。
二、HTML元素层级怎么拖动画的后腿?
很多人写HTML的时候,只会考虑语义,不会想层级对动画的影响。举个例子,你要做一个弹窗动画,弹窗是从底部弹出来的,你可能会这么写结构:
<!-- 商品列表 -->
<div class="product-list">
<div class="product-item">商品1</div>
<div class="product-item">商品2</div>
<!-- 很多商品项 -->
</div>
<!-- 弹窗 -->
<div class="popup">弹窗内容</div>
你觉得这个结构没问题,但如果商品列表里有几百个元素,而且每个商品项都有自己的动画,那弹窗弹出来的时候,浏览器会怎么处理?
2.1 浏览器的重排重绘逻辑
先给你讲个通俗的道理:浏览器渲染页面的时候,就像画家画画,得先画底层的,再画上层的。比如你先画了一整面墙(商品列表),然后要在墙上贴一个海报(弹窗),你得先把海报的位置留出来,再画海报。如果这时候你要移动海报(弹窗动画),你可能会不小心蹭到墙上的画,所以你得重新把墙和海报都画一遍。
对应到浏览器里,就是如果两个元素的层级有重叠,而且下面的元素是动态的(有动画),那上面的元素动的时候,浏览器会重新计算下面元素的位置、大小,再重新画一遍,这个过程叫重排重绘。重排重绘是很费性能的,尤其是元素多的时候,每动一次都要算几百个元素的位置,帧率自然就掉了。
2.2 错误层级的动画示例
我之前做的那个电商首页,商品列表有30个商品项,每个商品项的结构是这样的:
<!-- 错误的商品项结构 -->
<div class="product-item" style="position: relative;">
<!-- 商品图片 -->
<img class="product-img" src="xxx.jpg">
<!-- 商品标签,绝对定位在图片上 -->
<span class="product-tag" style="position: absolute; top: 10px; left: 10px;">新品</span>
</div>
然后弹窗是绝对定位,层级比商品项高:
<!-- 弹窗结构 -->
<div class="popup" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000;">
<!-- 弹窗内容 -->
</div>
这个结构的问题在于,商品项的标签是绝对定位,属于商品项的子元素,弹窗的层级比商品项高,所以弹窗弹出来的时候,浏览器会认为弹窗要覆盖所有商品项和它们的子元素。当弹窗做动画的时候,浏览器需要重新计算所有30个商品项、每个商品项的标签的位置,再重新画一遍,这个过程太费性能了,所以帧率掉了。
三、合成层数量对动画的隐性影响
浏览器为了优化动画性能,会把一些元素单独放到一个“合成层”里。合成层就像一个独立的图层,你动这个图层的时候,不会影响其他图层,浏览器只需要把这个图层的内容重新画一遍,再放到屏幕上就行,这样性能会好很多。但很多人不知道,合成层不是越多越好,太多了反而会更卡。
3.1 合成层的触发条件
先给你说通俗的:合成层就像你电脑里的独立窗口,每个窗口可以单独移动、缩放,不会影响其他窗口。浏览器会自动给一些元素加合成层,比如:
- 有3D变换的元素(比如
transform: translateZ(0)) - 有视频、Canvas的元素
- 有
will-change属性的元素 - 有
z-index很高的绝对定位元素
很多人会故意给动画元素加transform: translateZ(0),以为这样就能让动画变快,甚至给页面里所有的动画元素都加这个属性,结果合成层太多了,反而更卡。
3.2 合成层太多的动画示例
我之前做的那个电商首页,为了让商品项的动画变快,给每个商品项都加了transform: translateZ(0),结果页面里有30个合成层(每个商品项一个)。当弹窗弹出来的时候,浏览器需要把30个合成层的内容都重新处理一遍,再把弹窗的合成层放上去,这个过程的性能开销比重排重绘还大。
再给你举个更明显的例子:
<!-- 错误的动画元素,每个都加了合成层 -->
<div class="box" style="transform: translateZ(0);">1</div>
<div class="box" style="transform: translateZ(0);">2</div>
<div class="box" style="transform: translateZ(0);">3</div>
<!-- 很多这样的box -->
每个box都有自己的合成层,当你要移动其中一个box的时候,浏览器需要把所有合成层的内容都放到显存里,再重新组合到屏幕上。如果有100个合成层,那这个过程的开销是非常大的,帧率会直接掉到个位数。
3.3 合成层的正确使用
合成层不是不能用,而是要合理用。比如你有一个弹窗动画,你可以给弹窗单独加一个合成层,而不是给所有商品项都加。这样弹窗动的时候,只会处理自己的合成层,不会影响其他元素。
正确的做法是:
<!-- 弹窗加合成层 -->
<div class="popup" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; transform: translateZ(0);">
<!-- 弹窗内容 -->
</div>
这样弹窗自己是一个合成层,动的时候不会影响其他元素,性能开销就小很多。
四、怎么优化HTML层级和合成层?
说了这么多问题,那怎么优化呢?给你说几个实用的方法,都是我实际项目里用过的。
4.1 优化HTML层级的方法
- 把动态元素和静态元素分开:比如商品列表是静态的(除了商品项自己的动画),弹窗是动态的,那你可以把弹窗放到商品列表的外面,而且不要让弹窗覆盖太多静态元素。比如弹窗只覆盖屏幕的一部分,而不是整个屏幕。
- 不要用绝对定位的子元素在动态父元素上:比如商品项的标签,你可以改成相对定位,放到商品项的外面,这样弹窗动的时候,不会影响标签的位置。
- 减少层级重叠:如果两个元素不需要重叠,就不要让它们的z-index差太多。比如商品项的z-index是1,弹窗的z-index是1000,这样弹窗动的时候,浏览器会认为要覆盖所有商品项,开销很大。你可以把商品项的z-index改成0,弹窗的z-index改成1,这样重叠的范围小很多。
4.2 优化合成层的方法
- 不要给所有动画元素加合成层:只给需要动画的核心元素加,比如弹窗、滑块,而不是给每个商品项都加。
- 控制合成层的数量:页面里的合成层数量最好不要超过10个,太多了会导致显存不够,性能下降。
- 用
will-change代替translateZ(0):will-change可以告诉浏览器,这个元素要做动画了,提前给它加合成层,动画结束后,浏览器会自动把合成层回收,不会一直占着资源。比如:
.popup {
will-change: transform;
}
动画结束后,你可以把will-change改成auto,回收合成层:
// 弹窗动画结束后
popup.addEventListener('transitionend', () => {
popup.style.willChange = 'auto';
});
五、应用场景、优缺点和注意事项
5.1 应用场景
这些优化方法适用于所有有动画的网页,尤其是:
- 电商首页的商品列表动画
- 移动端的弹窗、导航动画
- 长页面的滚动动画
- 有大量元素的动态页面
5.2 技术优缺点
优化HTML层级的优点是:可以减少重排重绘的范围,提高动画性能;缺点是:需要重新调整HTML结构,可能会影响页面的布局。
优化合成层的优点是:可以让动画更流畅,不会影响其他元素;缺点是:如果用的太多,会导致性能下降,甚至出现内存泄漏。
5.3 注意事项
- 不要为了优化而优化:比如不要随便给元素加
transform: translateZ(0),只有当动画确实卡的时候,再考虑优化。 - 要在真实设备上测试:模拟器的性能和真实设备不一样,尤其是移动端,一定要在真实手机上测。
- 不要忽略合成层的回收:动画结束后,一定要把合成层回收,不然会占着显存,导致页面越来越卡。
六、文章总结
做网页动画的时候,不要只盯着CSS和JS,HTML的结构和合成层的数量对动画性能的影响非常大。很多时候,你改了半天CSS和JS,帧率还是上不去,就是因为没注意到这两个地方。
HTML层级的问题,本质上是浏览器渲染逻辑的问题,你要尽量减少动态元素和静态元素的重叠,减少重排重绘的范围。合成层的问题,本质上是浏览器资源分配的问题,你要合理控制合成层的数量,不要浪费资源。
最后给你一个小建议:做动画之前,先规划好HTML结构,再考虑动画的实现,不要等动画做出来了才去优化,这样会省很多时间。
评论
围绕“动画性能优化只关注CSS与JavaScript就够了吗,HTML元素层级与合成层数量对帧率的隐性影响同样致命”参与讨论