你一定知道 JS 动画的优先级 < css 动画。即使必要,用 JS 操作 class 的优先级也一定 > 用 JS 直接修改具体样式。
但是如果问到:“你了解 css 动画的性能么?如何优化?”你该怎么解决?
CSS 中有两个至关重要的概念 —— 重排和重绘。由此,通常会有一个问题:“为什么重排比重绘更耗性能?”
要解释这个,还要回到浏览器的渲染原理上:
自上而下解析 DOM,生成 DOM 树;
解析 CSS,生成 CSSOM 树;
(加载 js 代码)
DOM 树和 CSSOM 树节点一一对应,结合生成 Render 树;
渲染
布局
绘制
展示到图中是这样的:(流程 1)
其中 Painting 部分大有文章,还涉及到“千层饼”的产生。
重新排版。浏览器尺寸改变、元素位置和尺寸发生变化、新增和删除可见元素、内容发生改变、激活 CSS 伪类、JS 中查询某些属性或调用某些方法(如 offset
系、scroll
系、client
系 API)都会引起重排。总的来说,只要对其余元素产生影响的操作都会引起重排。
这么来看,页面首页渲染的时候是开销最大的一次重排了。
重新绘制。visibility
、outline
、颜色相关等属性的改变会引起重绘。
重绘不一定重排,重排一定重绘。
重排是整个页面的事!
结合上面那张图,可以发现,重排包含了 Render tree 形成的阶段。也就是 Layout(布局)阶段,一直到页面再次展现;而重绘操作只会引起 Painting 及后续的步骤。所以,从这方面来看,重排的确比重绘更耗性能。
此外,我们经常听到“尽可能地避免重排和重绘”。为什么呢?
事实上,在 Render tree 到视图真正展现在我们面前,浏览器实际上干了这些事:(流程 2)
浏览器会先获取 DOM 树并依据样式将其分割成多个独立的渲染层;
CPU 将每一层绘制进位图中;
将位图作为纹理上传至 GPU(显卡)绘制;
GPU 将所有的渲染层缓存并复合多个渲染层最终形成我们的图像。
可以看到,整套流程下来非常麻烦!
如果说上面那张图中包含了许多网页性能优化的秘诀。那么这几个点就可以说是 CSS 动画优化的关键!
结合上述内容,有几个点是必须注意的:
硬件加速
requestAnimationFrame
16ms 目标
渲染层
假如有一个div
,要把它移动到另一个位置:
刚开始的时候,浏览器会按照上面说的流程从上到下执行一遍,拿到 div
,现在我们去改变它的top
属性。当div
移动时,每移动一点,就会产生一张新的“位图”。也就是说,从 Layout 到 Composite Layers 的过程每次又要执行一遍。
那什么是硬件加速?
也叫 GPU 加速。因为 GPU 擅长对 texture 进行偏移、放大缩小、旋转等,而且将上面流程 2 放入流程 1 中我们可以看到:GPU 渲染时跳过了 Layout、Paint,只触发 Composite(也就是最后一步)。速度极快!
渲染速度快,动画也就会显得极其流畅!
不同 css 属性会触发的流程步骤个数也有不同,感兴趣的朋友可以参见官网:https://csstriggers.com
渲染层优化也和 GPU 相关 —— 它是通过构造一个单独的“渲染层(Layer)”,减小元素对其余元素的影响。
怎么做?
css 3d 或 perspective transform 属性
使用 animation、transition 改变 opacity、transform 等属性(不改变实际位置)
video、canvas、flash、css filter、z-index 大于某个相邻节点的值的元素 都会触发新的 Layer
后来提出的“给元素加上如下样式”:
transform: translateZ(0);backface-visibility: hidden;
复制代码
就是这个道理。
还能怎么做?
有节制地使用 will-change
可以让你的动画“更快”!它支持一些 css 属性比如 opacity、left、top、transform 等。目的是让浏览器更多地“注意”到这些属性(引起)的变化。它通常被认为是“开启 GPU 加速”。但支持度目前仍勉勉强强。
你可以还听说过“css 滚动视差”—— 它同样利用了这个原理。通过 perspective 和 transform 使元素处于不同的 Layer,以视觉效果达到“运动看起来不同步”的目的。
还是最初的例子,我们用setInterval
实现移动div
,由此产生的问题以及 css 方面的解决也已提到。但是,在一些场景中,我们必须使用 JS 操作动画/定时数据变化,怎么办呢?
我们知道浏览器作为一个复杂应用是多线程工作的。除了运行 JS 线程外,还有渲染线程、定时器触发线程、HTTP 请求线程等。JS 线程可以读取并修改 DOM,而渲染线程也需要读取 DOM。这是一个典型的多线程竞争临界资源的问题 —— 浏览器将它们设计成互斥的。这一点在之前的博客中有说过。
而requestAnimationFrame
的出现将它们关联起来了:通过调用requestAnimationFrame
我们可以在下次渲染之前执行回调函数。
那“下次渲染”具体是哪个时间点呢?简单来说就是在每一次 EventLoop 末尾,判断当前页面是否处于渲染时机。
那所谓的“渲染时机”是如何定义的?有屏幕的硬件限制,比如常见的60HZ
刷新率,就是 1s 刷新了 60 次,也就是大约 16.6ms 刷新一次。这个时候浏览器的渲染间隔时间就没必要小于 16.6ms,因为就算渲染了屏幕上也看不到 —— 当然,浏览器也不能保证一定会每 16.6ms 渲染一次,因为还会受到处理器性能、JS 执行效率等其它因素影响。
回到requestAnimationFrame
,这个 API 保证在下次浏览器渲染前一定会被调用。它的时间是由浏览器自己不断调整的。
requestAnimationFrame(function(){ setInterval(function(){ var top = parseInt(box.offsetTop); box.style.top = (top - 1) + "px"; })})
复制代码
但是,在一些需要及时更新数据的场景下,还是要通过上面说的“强制触发渲染的 APIs”让渲染间隔在 16.6ms 以内,以提高下一步操作的能力。
事实上,随着前端的发展。一方面,css 可以结合更加复杂的函数、做更加复杂的动画(参考“第五人格”皮肤页光影滑过特效;在复杂场景下这里面还涉及明暗色对比以及线性代数的存在);
另一方面,动画的产生也不一定完全由前端实现。如果由 UI 制作好再以代码导入的方式实现无疑我们只需要更加关注动画的加载、性能以及更多的可能性 —— 我曾经研究支付宝关于新春活动的文章,里面有一副图让我印象深刻:(旋转并有粒子特效,看起来更“真实”)
当我停下来思考这样的动画如何高性能打造无果后发现他们采用了:
import myAnimation from '../assets/my-ani.vfx'; // 网页编辑项目工程文件
const player = new Player({ container:document.getElementById('displayObject') }); player.loadSceneAsync(myAnimation).then(scene=>player.play(scene));
复制代码
将编辑器工程作为资源引入,直接播放就可以了。
文章 中说:“最好的还原方法是不写代码。编辑器直接导出动画数据,在手机上进行播放,开发完全不用关心各种参数。而产物很容易使用,直接保存项目工程,通过 webpack 进行加载,像使用图片一样简单。”
恍然大悟,兴奋不已。
版权声明: 本文为 InfoQ 作者【云小梦】的原创文章。
原文链接:【https://xie.infoq.cn/article/61587dfeceac0eabe21d292e4】。文章转载请联系作者。