作者:三羊
来源:https://snayan.github.io/post/2019/in_depth_animation_frame/
在前端性能优化策略中,耳熟能详的手段有,雅虎 35 条军规,使用 cache,减少请求数量,使用cookie-free domain,critical asset,使用 CDN,Lazy load,PreLoad 等,这些手段其实主要都是围绕怎么样更快的拿到所需关键资源。当我们把这一步做到很好,没有可优化空间了,其实还可以从另外一个方向去做优化,那就是浏览器渲染方面。浏览器渲染优化是一个很大的主题,今天,我们只谈它一个小角度,动画帧。从动画帧,我们可以怎么样来做一些优化工作呢?本文篇幅较长,图较多,耐心看完,一定会有很多收获的。
基础概念
先还是来熟悉基础概念。帧,可以理解为浏览器每一次绘制的快照。1s 内绘制的次数,叫做帧率,也就是我们常说的 fps(frame per second)。帧率越大,浏览器在 1s 内绘制的次数就越多,动画就越流畅。人们视觉系统对帧率的最低要求一般是 24fps,当帧率低于 24 时,就会感觉到明显的卡顿了。不同的移动设备,有不同的帧率,一般默认是 60fps。
我们要追求的理想帧率是 60fps,那么一帧绘制的时间最多是 1 / 60 = 16.7ms,一旦超过这个数,就达不到 60fps 了。为了使得一帧花费的时间控制在 16.7ms 内,我们必须先搞清楚浏览器在一帧会做些什么事情呢?
帧任务
html,js,css 是浏览器处理最为常见的三种资源,也是我们前端工程师每天都会打交道的文件。可是我们真正知道浏览器是怎么绘制的吗?稍微思考一下,然后会说,html 解析为 dom tree,css 解析为 cssom tree,然后 dom tree 加上 cssom tree 就合并为 render tree,最后浏览器根据 render tree 绘制到屏幕上。这是我们最为熟知的步骤,但是它只是描述了一个整体大致的过程,并没有具体到一帧的绘制过程。下面看看一帧具体绘制的过程图。
上图就是浏览器在绘制一帧时,会经过的处理步骤。
JavaScript,我们会通过 js 来改动一些视觉效果,比如基于 js 的动画,或者响应用户事件等。
Style,如果通过 js 改变了某一个 dom 的样式,就会重新计算受影响的元素的样式。
Layout,如果样式改变中涉及了布局属性,例如 top,left,width 等几何位置,还会重新计算它的布局位置,以及受影响的其他元素的布局。当然,如果不涉及布局样式,也不会执行这一步。
Paint,计算出 Style 和 Layout 后,就可以把元素绘制到它所属的 paint layer 上。
Composite,最后会将多个 composite layer 输出到屏幕上显示出来。
要控制一帧的执行时间在 16.7ms 内,就只需要把上述 5 个步骤处理时间总和控制在 16.7ms 内。接下来,我们一个步骤一个步骤来看,有哪些优化建议。
JavaScript
requestAnimationFrame
JavaScript 是前端工程师的利器,有了它,我们可以实现非常复杂的系统,或者非常流畅的游戏等。JavaScript 通常会处理用户输入或者点击等事件,然后做一系列的视觉效果的改变。当涉及 dom 元素改变时,总是把操作放在requestAnimationFrame
中。
requestAnimationFrame
会有什么神奇之处呢?
它总是会确保 fn 的执行在浏览器一帧的开头
浏览器会自动调节帧率,间接调节了 fn 的执行频率
在不支持 raf 的浏览器中,通常使用setTimeout(fn, 16.7)
来实现。但是setTimeout
有一些不好的地方,
它并不是保证 fn 每隔 16.7ms 就执行一次,它只能每 16.7ms 将 fn 加入到MacroTask Queue中,具体什么时候执行,要根据当前执行队列决定
它不能保证每次执行时机都是一帧的开头,可能某一次执行触发是在帧的中间或者结尾,导致延长当前帧在 16.7ms 内无法执行完成,就会出现丢失当前帧
不同设备帧率不一样,并不是固定的 60fps,给低帧率(比如 30fps)的设备上执行
setTimeout(fn, 16.7)
将会导致执行很多无意义的 fn
我们先使用setTimeout
来实现,每 16.7ms 就更