【译】使用 will-change 目标让浏览器拥有 60fps 如丝般流畅的 css 动画

2,515 阅读9分钟

大家好、这里就是用了大量动画去做个人主页、还有射击游戏的 yuki

感觉 css 的动画用的太多了,我的 MacBook 都要“罢工”了,回过头来,我就给大家总结一下,我的实验结果吧。(还是好想用 css 动画

这次的例子

这篇文章的目的大概就是,在网页里用 css 动画加一点复杂的动画,然后去做一些游戏或者艺术表现的时候,全部都用上 css 的 will-change 属性,怎么样才能让 GPU 渲染最合适。

如果你要问 will-change 是什么?那可以看一下我放在这篇文章里的参考列表。

示例

css-anime.firebaseapp.com/

本次要验证的动画有下面几个点:

  • 用 css 画 1000 朵花❀,一朵一朵的做开画的动画
  • 每朵花都是 div 元素(这次不用 svg)
  • 每次间隔 32ms 画一朵
  • 开完之后的花,就保持停留在画面上
  • 并且画面一直会在旋转

不知道大家有没有遇到过,需要一边做动画,还得一直加标签,然后还得一直显示,且不能从画面消失,这种很痛苦的情况。

实验环境如下:

  • MacBookPro 2017Late / 8GB RAM(经常被叫做“梅”的最低配置)
  • MacOSX Mojave
  • Chrome 74

之后会提一下 Safari 下的情况。 因为不同的环境会有很大的差异,浏览器和操作系统不一样的话,结果会有差异,我先在这里事先说明一下。

站在巨人的肩膀:我觉得值得一读的文章

译者注:下面链接全部都是*日文*的,标题我翻译了一下,日文 ok 的同学可以点点看...

这篇文章参考以下链接:

实验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 属性。

原文地址:qiita.com/yuneco/item…