技术

浏览器渲染管线以及动画性能学习

重排、重绘、合成

背景

重排、重绘,相信大部分人遇到这个问题都会有类似的回答:重排性能消耗大,浏览器会重新计算文档布局。重绘性能消耗小一些,浏览器不需要重新计算布局,但是需要重新计算像素数据。 然后再举几个例子:width height重排啦,color是重绘巴拉巴拉...

最近看到Motion作者采访React Bits组件库作者的一篇访谈,顺藤麻瓜看到了Motion作者的另一篇文章,针对浏览器渲染原则有了更深入的理解,如果有英文阅读水平,强烈建议阅读这篇文章: 浏览器动画性能排名从夯到拉.

这篇博客文章基于本人的理解进行一些摘要、汇总和补充. by the way,这整个博客系统的动画效果都是motion写的,确实符合react的编码习惯,是一个很好的动画库. 之后也会继续学习别的动画方式,之前简单过了一下Three.js,感觉有些碰壁,暂定将目标转向GSAP,然后是Threejs、WebGl、WebGpu这哥三.

渲染管线

先说一个老生常谈的问题,大家都知道浏览器处理JS代码是单线程的,不当的代码会造成线程阻塞引起性能问题. ok,那什么是渲染管线呢?

总的来说,渲染管线就是将网页元素(html、css等东西)转换为页面上展示图像的通道。 这个通道按照一定顺序执行:JS -> Style -> layout -> paint -> composition. 一个很重要的点是,触发是链式的,比如说触发了paint,那么一定会联动触发composition,这也是为什么JS改class会触发后续的样式变化. 可以类比react中父组件重渲染一定引发子组件重渲染的逻辑.

触发了layout,就是重排. 触发了paint,也就是重绘. composition合成层一般很少提到. 一个比较常见的情况是transform,它会调用gpu进行处理,大部分情况都是composition层处理的,有一小部分会引发paint,之后的章节会谈到.

需要注意的是,composition运行在一个独立的线程上. 而layout、paint和js等一堆东西运行在主线程上. 我们都知道主线程是很可贵的,它还需要运行js等内容. 如果想要动画效果更加流畅,我们需要尽量触发composition层的动画,避免触发在主线程中的paint甚至layout层面的变化. 我相信此时你已经明白了使用transform进行width height变更的理由.

这里重点介绍一下陌生的composition阶段,这个阶段的核心工作是图层的合成以及位图操作. 意思就是说,composition阶段将页面拆分,变为多个图层,然后将这些图层(也就是位图)叠加在一起,最终呈现在页面上.

高质量动画

通过刚才渲染管线的学习,已经了解了浏览器处理动画的逻辑。我们可以得出结论:高性能动画的本质就是尽量避免触发paint甚至layout阶段,尽量使用composition阶段处理动画. 那么在实际开发中怎么判断,这个属性会触发哪些阶段呢?

我认为这个完全依赖于经验,没有一个统一的结论... 可以简单归纳的是,composition阶段主要涉及变换、透明度,比如transform, opacity, 以及浏览器的scroll等. paint阶段主要涉及元素的外观和颜色, 比如color, border-radius, box-shadow等等. layout阶段涉及元素的形状、位置, 比如常用的width, margin, top, right这些.

还记得上一节中的图层叠加吗? 这里有一个特例:svg。svg使用一个独立的坐标系绘图,如果针对svg进行transform,一般无法跳过paint阶段. 原因是这样的:一个普通的元素(比如div标签吧)会被绘制成一张位图,之后针对这个元素的transform动画其实是在操作这张位图进行稽几何变换. 但是针对svg操作时(比如内部一个path标签),这个标签不代表一个完整的合成层,浏览器会将整个svg变为一张矢量图,当使用transform修改path时,相对于整个 SVG 画布,图像内容变了。浏览器必须擦除原来位置的svg,并在新位置重新绘制这个矢量路径。(备注:针对svg根节点transform,此时表现和div一样的,因为都是操作一个完整的图层)

FLIP动画

之前提到我们尽量让动画运行在composition层,但是如果我们需要修改一个绝对定位元素的位置呢?此时似乎只能通过top、left...进行定位了.

后来了解到了一种叫做FLIP形式的动画形式,这个FILP的核心目的就是将layout动画转换为composition层面的动画,而且竟然早在十年前就已经提出了.. 那么FLIP代表了什么含义呢? F和L很好理解,就是first和last的缩写,代表了元素的初始状态以及动画结束后的终点状态. I代表了invert,计算了起始态和终止态的delta量,这个delta可能包含了宽高、缩放比例等等. 然后通过transform将终点态的元素瞬时移动到起始态的位置和大小,此时在视觉上,这个元素还是停留在初始状态,但是实际已经完成了移动操作. 最后是P,代表了play,移除I阶段的transform,让元素从视觉上的初始状态移动到真正的终点态.

可以预见的是,FLIP听起来效果很美好,但是需要写大量的js代码来计算位置,有没有web API能自动实现这一功能呢? 有的兄弟,有的。 可以查看Transition这个API: 视图过渡 API

这是一个比较新的api,注意兼容性. transition兼容性各个api兼容性 transition兼容性can i use

另外关于FLIP动画,可以看这两篇文章,讲的很具体^^
FLIP Your Animations.
Animating Layouts with the FLIP Technique.