事件循环->布局颠簸->动画性能

tianyn1990

一、事件循环&定时器

事件循环(Event Loop) 是指浏览器通过不断循环检查任务队列,使单线程的javascript语言执行异步任务的过程 文章链接

  1. 上文的两张图

  2. 「定时器-问题三」、「requestAnimationFrame」:如果浏览器性能不佳,setTimeout发生跳帧(其实就算浏览器性能良好也会发生跳帧,不可避免),Raf会造成动画变慢

  3. 下文我们重点探讨的动画都是js动画

  4. 既然css3动画又快又好,还用js动画(包括svg)干嘛?原因大概包括如下一些:

    • css动画不能完全被js控制
    • 贝塞尔动画有局限(duang…duang..duang.duangduang)
    • 让css做个飞入购物车的抛物线?(同上2条)
    • 让css控制滚动条?(css只能改样式,不能控制浏览器行为)
    • css会与js/html耦合
    • 大量使用GPU硬件加速,浏览器会高负荷运转
    • GPU与CPU数据传输也需要时间
    • 兼容:IE10以下的浏览器不支持transition。而目前 IE8 和 IE9 还是很流行的
    • 传闻js动画经过良好优化后性能反而可超css3,见js动画库velocity.js作者文章,译文
    • 使用velocity.js的js动画举例 demo demo2

当然,如果开发简单动画效果,css3仍是高性能的原生支持方案

补充:css3动画的回调

Raf虽然在保证了「浏览器刷新频率和DOM移动频率相互同步」的问题,但由于js动画性能不良(?)以及开发难度较高(!),我们常常使用css3标准实现动画效果。

由于帧率不确定造成了动画完成时间也不确定,因此使用setTimeout(cb,delay)的方式来执行css3动画的回调函数,可能造成提前或延后,且动画的css与js也发生了耦合。

因此我们可以使用transitionendanimationstartanimationiterationanimationend事件。见陆忠芳的早读

至于兼容性的问题,基本上兼容transition/animation的浏览器同时也兼容对应事件,因此也不必纠结!

二、布局颠簸 & fastDOM

1、什么是?

需要用到上文的知识

如果在同一帧中,进行了「读-写-读」的操作,就会引发布局颠簸(Layout Thrashing),举个最经典的栗子:

1
2
3
4
5
6
7
8
9
10
11
// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers layout)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

当write操作发生以后,对应的布局就会失效,需要进行重新计算并reflow,浏览器原本会将这个「耗时」的操作放到下一次刷新中再统一进行一次reflow操作。

而在上面的代码中,write之后接着进行了read,为了保证读取的数据是正确的,会迫使浏览器在当前这一帧中提前进行reflow,于是「布局颠簸」就产生了。

如果这种事情在一帧中大量发生,那么对性能的影响是巨大的。可以参考一个例子>>todo:fastdom/**/aspect.html;

上文中,我们了解到根据浏览器时间精度的不同,使用requestAnimationFrame所注册的一个异步任务(也就是回调函数)会在下一次浏览器检查异步任务队列的时候触发。

如果这个回调进行了DOM操作,由于浏览器刷新(/repaint/layout 随便怎么称呼)一次的时间间隔一般都是1000/60ms,因此DOM操作也将在「下一帧」才能生效!

于是我们可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

requestAnimationFrame(function(){

// Read
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';
});

在每一帧中,先执行所有read操作,再执行所有write操作,当有新的read操作需要发生在write之后执行时,我们将它移入到下一帧执行
(使用requestAnimationFrame,不兼容则用setTimeout(cb,0))

chrome也在Timeline中对强制同步操作(布局颠簸)进行了提示 链接

2、「然而」来了

然而,在现实生活中,没有人像上面那样写代码,并且:

1)代码中经常读写操作穿插在一起,如果将读写操作分开,会打乱代码书写逻辑,造成程序员理解异常。还有的时候读写顺序无法改变

2)代码都是按照模块解耦的,模块A在进行读操作的时候,并不知道在此时此刻的这一帧中,另一个模块X是否刚刚进行了写操作

3)更重要的是,如果有多个js动画(包括svg动画)同时执行,有可能造成大量Layout Thrashing,严重影响动画性能

3、如何解决?

首先,对于动画来说,缓存动画元素的各种属性值,尽量在动画开始的时候进行一次读操作就可以了,一般动画都是这样做的,

但需要注意的是,在链式动画过程中,前一个动画的结尾属性也需要缓存,以减少下一个动画的读操作。

但这还不够,在各种非动画场景下,我们也会经常大量的操作DOM(使用jquery或nej),LT不可避免。

于是类似fastDom.js这样的框架就出现了,它的原理就是利用requestAnimationFrame,将会产生LT的部分挪到下一帧中再执行,
而在同一帧中,先批量执行所有读操作,再批量执行写操作。

具体请看:

例子>>todo:fastdom/**/test.simple.fastdom.html;

源码>>todo:fastdom/**/fastdom.js;

4、实际运用fastdom

有个小哥在项目中用了fastdom,这里是他的经验教训

他还将这个思想运用到了组件接口的设计中。这个
项目尝试分离了读&写的API(calc读,set写),来帮助使用者合理控制读写,但在大量使用读写时,仍无法保证不触发LT。

他还遭遇了另一个问题,由于使用的是单页面app,有时候在移除一些组件的时候,组件节点直接被移除了,
但fastdom将DOM操作放到了一下帧,当执行的时候发现组件节点已不存在,于是报错。
他们最后通过实例化fastdom,并扩展了一个clear方法(取名instantiable-fastdom
来解决。

另外fastdom在后来的版本中直接try-catch掉了这种异常。

三、动画性能

由于只做了很少的性能测试,也没有深入了解一些动画框架的原理,因此只能总结一点简单的理解。

js动画

在解决布局颠簸问题,并且缓存各种状态之后,我们还有以下可能提升性能的地方:

  1. 忽略过于微小甚至不可见的状态改变(<1px)
  2. 根据实际情况主动触发GPU硬件加速
  3. raf

css动画

上面虽然列举了一系列的问题,但css动画还是又快又好。

2015 Google I/0 的官方站点中,
使用了一种被称作FLIP的原则开发动画效果链接,译文