实现达到 60FPS 的高性能交互动画

译者注:这篇大部分是老生常谈,但也稍微有一些新东西呢,要看到最后哦 =)

高性能的 Web 交互动画:如何达到 60FPS

每一个追求自然效果的产品都希望拥有一套顺畅的交互流程。但开发者可能会忽略一些细节,导致出现性能糟糕的 Web 动画,不仅会产生“页面垃圾”(janky),最直接的体验就是页面卡顿。开发者往往会花大量精力在优化首屏加载,为了几毫秒锱铢必较,但忽略了页面交互动画所带来的性能问题。

Algolia 的每一位同事都很关注用户体验,「性能」一定是这个话题里无法回避的关键部分。动画性能之于页面的重要性,就像搜索结果速度之于搜索一样。

成功的标准

动画帧率可以作为衡量标准,一般来说画面在 60fps 的帧率下效果比较好。换算一下就是,每一帧要在 16.7ms (16.7 = 60/1000) 内完成渲染。因此,我们的首要任务是减少不必要的性能消耗。 越多的帧需要渲染的,意味着有越多的任务需要浏览器处理,所以掉帧就出现了,这是达到 60fps 的一个绊脚石。如果所有动画都无法在 16.7ms 渲染完毕,不如考虑用略低的 30fps 帧率来渲染。

浏览器 101:像素是怎么来的

在深入研究之前,我们要先搞清楚一个很重要的问题:浏览器是怎么把代码转化成为用户可见的像素点呢?

首次加载时,浏览器会下载并解析 HTML,将 HTML 元素转变为一个 DOM 节点的「内容树」(content tree)。除此之外,样式同样会被解析生成「渲染树」 (render tree)。为了提升性能,渲染引擎会分开完成这些工作,甚至会出现渲染树比 DOM 树更快生成出来。

首次页面加载时的 call tree

布局

渲染树生成后,浏览器会从页面左上角开始迭代地计算出每个元素尺寸和位置,最终生成布局。这个过程可能是一气呵成的,但也可能由于元素的排列导致反复地绘制。元素间的位置关系都紧密相关。为了优化必要的任务,浏览器会追踪元素的变化情况,并将这些元素以及它们的子节点标记为 ‘dirty’(脏元素)。但是元素间耦合紧密,任何布局上的改变代价都是重大的,应该尽量避免

绘制

生成布局后,浏览器将页面绘制到屏幕上。这个环节和「布局」步骤类似,浏览器会追踪脏元素,将它们合并到一个超大的矩形区域中。每一帧内只会发生一次重绘,用于绘制这个被污染区域。重绘也会消耗大量性能,能免则免

复合

最后一步,将所有绘制好的元素进行复合。默认情况下,所有元素将会被绘制到同一个层中;如果将元素分开到不同的复合层中,更新元素对性能友好,不在同一层的元素不容易受到影响。CPU 绘制层,GPU 生成层。基础绘图操作在硬件加速合成中完成效率高。层的分离允许非破坏性的改变,正如你所猜测的,GPU 复合层上的改变代价最小性能消耗最少

激发创造力

一般情况下,更改复合层是相对消耗性能较少的一个操作,所以尽量通过改变 opacitytransform 的值触发复合层绘制。看起来好像…我们能做出的效果会很有限,但真的是这样吗?要好好开发自己的创造力哦。

变换

「变换」为元素提供了无限的可能性:位置可以改变 (translateX, translateY, 或 translate3d)、大小也可以通过缩放 (scale) 改变、还能旋转、斜切甚至 3D 变换。就是在某些场景下,开发者需要换一种思考方式,通过使用变换减少重排和重绘。 比如给一个元素添加 active 类名后它会向左移动 10px,可以通过改变 left 属性:

也可以用能够达到相同效果但性能更好的 translate

透明度

可以通过改变 opacity 的值,实现元素的显示和隐藏(与改变 display 或者 visibility 的值达到类似的效果类似,但性能更好)。比如实现菜单的切换效果:菜单展开时,opacity 值为1;收起时,opacity 值变为 0。要注意的是 pointer-events 的值也要随之改变,防止用户操作到明明收起的菜单。closed 类名会根据用户点击 ‘open’ 时,closed 类名会被加上;点击 ‘close’ 按钮时,closed 类名会被移除。对应的代码是这样的:

另外,透明度可变意味着开发者可以控制元素的可见程度。多多思考应用透明度的场景 — 比如直接给元素的阴影 (box-shadow) 做动效很可能会造成严重的性能问题:

如果把阴影放到伪元素上,控制伪元素的透明度从而控制阴影,效果一样但性能更好,代码如下:

手动优化

还有一个好消息 — 开发者可以选择想要控制的属性,创建复合层,并将元素拖到该层。通过手动优化,确保元素总能被绘制好,这也是通知浏览器准备绘制该元素的最简单方式。需要独立层的场景包括:元素的状态将发生一些变化(比如动画)、改变了很消耗性能的样式(比如 position:fixedoverflow:scroll)。可能你也见过了糟糕的性能导致了页面闪烁、震动…或其他不如预期的效果,例如移动端常见的固定在视口顶部的头部,会在页面滚动的时候闪烁。将这样的元素独立到自己的复合层,就是常见的解决这类问题的方法。

hack 方法

从前,开发者通常是通过 backface-visibility:hidden 或者 trasform: translate3d(0,0,0) 触发浏览器生成新的复合层,但这并不是标准的写法,这两种写法也对元素的视觉效果不起作用。

新方法

现在有了will-change,它能够显式地通知浏览器对某一个元素的某个或某些元素做渲染优化。will-change 接收各种各样的属性值,比如一个或多