大家好、这里就是用了大量动画去做个人主页、还有射击游戏的 yuki
。
感觉 css 的动画用的太多了,我的 MacBook 都要“罢工”了,回过头来,我就给大家总结一下,我的实验结果吧。(还是好想用 css 动画
这次的例子
这篇文章的目的大概就是,在网页里用 css 动画加一点复杂的动画,然后去做一些游戏或者艺术表现的时候,全部都用上 css 的 will-change 属性,怎么样才能让 GPU 渲染最合适。
如果你要问 will-change 是什么?那可以看一下我放在这篇文章里的参考列表。
本次要验证的动画有下面几个点:
- 用 css 画 1000 朵花❀,一朵一朵的做开画的动画
- 每朵花都是 div 元素(这次不用 svg)
- 每次间隔 32ms 画一朵
- 开完之后的花,就保持停留在画面上
- 并且画面一直会在旋转
不知道大家有没有遇到过,需要一边做动画,还得一直加标签,然后还得一直显示,且不能从画面消失,这种很痛苦的情况。
实验环境如下:
- MacBookPro 2017Late / 8GB RAM(经常被叫做“梅”的最低配置)
- MacOSX Mojave
- Chrome 74
之后会提一下 Safari 下的情况。 因为不同的环境会有很大的差异,浏览器和操作系统不一样的话,结果会有差异,我先在这里事先说明一下。
站在巨人的肩膀:我觉得值得一读的文章
译者注:下面链接全部都是*日文*的,标题我翻译了一下,日文 ok 的同学可以点点看...
这篇文章参考以下链接:
- CSS Will Change Module Level 1(日文版)
- CSS: will-change 对性能的影响(日文版)
- 高性能 60fps 的 CSS3 动画最佳实践(日文版)
- 优先使用合成专用的属性,以及层级数的管理(日文版)
实验0:基本元素与动画的构建
为了下手方便,我就先随便的写了一下,我把代码贴也上来了,大家看到代码应该也就就明白了。
花❀的话 html 部分大概就是这样的,为了轻松和个人兴趣,这里用了下 Vue,其实什么框架都是可以的。
Flower.vue
<div class="flower-root"
:class="{animate: visible}"
@animationend="onEndAnim">
<!-- 花瓣 -->
<div class="petal" v-for="(petal, index) in petals" :key="index"
:style="{
transform: `rotate(${petal.r}deg)`,
'background-color': petal.col
}">
</div>
<!-- 中间 -->
<div class="center"></div>
</div>
花瓣是用 div 搭的,除了角度和颜色是在 template 里面写的,其它的都是在下面 <style> 标签里写的。开花的动画也在这里写了(也就是说,动画跟 Vue 和 javascirpt 没有关系,就只是用 css 写的而已)。
Flower.vue
<style lang="scss" scoped>
.flower-root {
position: absolute;
transform: rotate(0deg) scale(0);
animation: rotate 2s ease-out 0s 1 normal forwards;
}
.petal {
position: absolute;
width: 70px;
height: 20px;
top: -10px;
left: 0;
transform-origin: left center;
border-radius: 50px;
background-color: #ffb7aa;
}
.center {
position: absolute;
width: 30px;
height: 30px;
left: -15px;
top: -15px;
border-radius: 30px;
background-color: #ffe683;
}
// 一边旋转一遍变大
@keyframes rotate {
0% { transform: rotate(0deg) scale(0); }
100% { transform: rotate(360deg) scale(1); }
}
</style>
写出来的花大概就是这种感觉。
让这个 Flower 组件每隔一定时间,就往画面里加,然后画面的整体让它转起来。
实验1:直接跑起来(没有写 will-change)
先试试不用 will-change,跑一下动画,打开 Chrome 的 performance monitor 面板。
译者注:可以 f12 -> ctrl + shift + p 输入 rendering 打开 fps 面板,图片上左侧的 gpu 面板是 mac 系统自带的。
CPU 还挺给力的,还保持着 60 fps,哦哟,好像还不错哟。
500 个了。差不多 400 个左右的时候 CPU 已经到达瓶颈了,帧数一下子就掉下来了,浏览器上了 GPU,但是帧数好像还是很不稳定。
最后,平均帧数到了差不多 20fps,风扇一直转的大声...
所有的花都开完了,只剩下 1000 朵静止的花在旋转。到这一步,终于回复了 60fps。
实验 1 结论
- css 动画会因为元素的比例上升,CPU 负荷会上涨。
- CPU 到达瓶颈的时候,帧数会暴跌。
实验2:全部都用上 will-change
但是不试试看怎么会知道呢?变更的地方就是在 style 里加上 will-change: transform。
Flower.vue
.flower-root {
position: absolute;
transform: rotate(0deg) scale(0);
animation: rotate 2s ease-out 0s 1 normal forwards;
will-change: transform; // 追加
}
Let's start!
第 100 个,非常的流畅,CPU 负荷也很低。
500 个,开始有点挣扎了,GPU 就像吃了芥末一样,一下子就窜起来了,CPU 也有点负荷了。
快到结尾了,CPU 和 GPU 都在疯狂的“惨叫”,performance 也是很危险的状态。
所有的开花的动画都结束了之后,负荷也不会降下来, 写了 will-change 的话,浏览器为了保证接下来的随时会开始的动画的性能,就算动画结束了,负荷也不会下降。
实验 2 结论
- 写了 will-change 的话,就会启动 GPU 加速
- 带 will-change 的元素过多的话,GPU 和 CPU 都会加重负担
- 写了 will-change 的元素,就算动画结束了,仍然不会减轻负担
实验3:动画结束后,删除 will-change 样式
译者注:把 will-change 属性删除是指把 will-change 的指设置成默认的 auto,之后的翻译也都是如此。
实验 2 的问题是,开花的动画明明都已经结束了,但是 will-change 却存在着。因此,在实验 3,开花的动画结束后,就把 will-change 样式删除。
为了能够动态的设置 will-change 属性,所以把这个属性挪到 template 里了,然后加个变量去控制它(在 Vue 加了个 isMoving 变量)。监听 animationend 事件,在这个事件内,去改 isMoving 变量。
Flower.vue
<div class="flower-root"
:style="{
'will-change': isMoving ? 'transform' : 'auto',
}"
@animationend="onEndAnim">
...
Flower.vue
private onEndAnim() {
this.isMoving = false
}
检测到动画结束,只把 will-change 的值删除。
那么,start!
第 100 个,感觉不错...
500 个,嗯?CPU 好吃力啊。帧数也差不多 300 个左右的时候就降下来了。因为动画结束了就把 will-change 给删了,GPU 负荷是轻了,但是 CPU 负担却重了。
到最后,差不多要“罢工”了。
所有的动画结束了之后,终于回复到 60fps 了。
到底发生了什么?
好奇怪,结果并没有那么明朗。 我只是在动画结束后马上把 will-change 的值删了而已,到底发生了什么?
看了下 Chrome 的 performance 的情况,Update Layer Tree 很迷,隔一段时间执行一次。
这是 Chrome 内部的处理,我也不太知道具体是为什么...
看 DevTools 的 Timeline 面板、理解浏览器的渲染机制(日文链接)
Update Layer Tree
GPU 更新着的执行处理的 layer
就是说,GPU 为了执行处理,把需要处理的元素放到 layer 上,把不需要处理的元素从 layer 删掉,然后重新构建。
这次试错的结果,现在的这个情况,Chrome 会有以下会倾向的点:
- 依赖 layer 的元素,会使 Update Layer Tree 变重。
- 比起升级到 layer,从 layer 降级更加耗费性能,也就是说删除 will-change 的工作很费性能。
我没有看源码,这些也只是我的猜测。
实验 3 结论
- 动画结束了之后把 will-change 属性删掉,会影响 GPU 的负荷
- GPU 负荷高的时候,再设置 will-change 的值会很吃力,特别是把 will-change 的指删掉
- 高负荷状态下,动画结束了,一个一个的去删 will-change 的值,无疑是致命的
实验 4:某种程度下,删除 will-change 的值
在实验 3 中,动画结束,一个一个的去删 will-change 的值,效果会非常的糟糕。不知道为什么 Chrome 要在这个点上这么的“努力”啊,我试试看有没有其它办法绕一下。
既然执一次一次去删 will-change 的话,会让 Update Layer Tree 产生负担,那么 100 个的话也是一样的吧。那么在攒一定数量之后一口气的去删掉的话会是怎么样呢?马上来试试看。
准备好 Flower 队列
Flower.vue
const queueLimit = 100
const stopedFlowers: Flower[] = []
动画结束后,加到队列里, 到 100 个了之后,设置 isMoving 的值,一口气删掉 will-change 的值。
Flower.vue
private onEndAnim() {
stopedFlowers.push(this)
if (stopedFlowers.length === queueLimit) {
stopedFlowers.forEach(fl => fl.isMoving = false)
stopedFlowers.length = 0
}
}
感觉有点像是在投机取巧,哈哈。实验开始!
第 100 个,这个时候动画才开始,will-change 全部都是带着的,也就是说,现在的这个状态跟实验 2 是一样的。
第 500 个,以 100 个为单位,删除 will-change,所以会隔一段时间,帧数会掉一下。
到最后这种情况会一直持续,虽然会有那么一瞬间帧数会卡一下,单总的来说,负荷还是很平衡的。
安全跑完,最后 100 个结束了之后,所有的元素都会删掉 will-change 的值。这时候,跟实验 1 和实验 3 的状态是一样的。
实验 4 结论
- 如果 will-change 在动画结束后,攒到一定程度,再去删除的话,效果会比较好。
- 删掉 will-change 属性的那一瞬间,没办法避免负荷会变重是的(限本次实验范围)。
- 在大量使用动画的时候,我认为是有必要看准时间去删掉 will-change。
最后顺便测一下 safari 下的情况吧
在 MacOS/iOS 的 Safari 跑了下,实验 3 的结果非常的流畅。
也就是说,如果不是 Chrome 的 bug 的话,难道是我使用的问题?因为没有看源码,所以也不太确定。如果有知道的同学,欢迎在评论区评论。
总结
- 想流畅的使用 GPU 做 CSS 动画的话,加上 will-change 属性,提升体验。
- will-change 在动画结束的时候,删掉这个属性,降低负荷。
- 在 Chrome 下,把 will-change 删掉的话,Update Layer Tree 会重新构建 layer,负担会变重。可以每隔一段时间去删掉 will-change,可以把影响降到最低。
- 多确认下不同的浏览器和不同的环境。不要总是想着“Chrome 就是对的”。
- 夺取确认性能监测面板,还有系统的性能监测面板。就算有 60fps,说不定你的机器正在“惨叫”...
译者记
之前只是知道 will-change 会让浏览器启用 gpu 渲染,没想到这个使用多了,不一定就会爽了,使用 will-change 也是有很多需要注意的点。如果在需要大量使用动画的情况下,可以参考下这篇文章的做法,适当的去删掉 will-change 属性。