一、事件循环&定时器
事件循环(Event Loop) 是指浏览器通过不断循环检查任务队列,使单线程的javascript语言执行异步任务的过程 文章链接
上文的两张图
「定时器-问题三」、「requestAnimationFrame」:如果浏览器性能不佳,setTimeout发生跳帧(其实就算浏览器性能良好也会发生跳帧,不可避免),Raf会造成动画变慢
下文我们重点探讨的动画都是js动画
既然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也发生了耦合。
因此我们可以使用transitionend
、animationstart
、animationiteration
、animationend
事件。见陆忠芳的早读
至于兼容性的问题,基本上兼容transition/animation的浏览器同时也兼容对应事件,因此也不必纠结!
二、布局颠簸 & fastDOM
1、什么是?
需要用到上文的知识
如果在同一帧中,进行了「读-写-读」的操作,就会引发布局颠簸(Layout Thrashing),举个最经典的栗子:
1 | // Read |
当write操作发生以后,对应的布局就会失效,需要进行重新计算并reflow,浏览器原本会将这个「耗时」的操作放到下一次刷新中再统一进行一次reflow操作。
而在上面的代码中,write之后接着进行了read,为了保证读取的数据是正确的,会迫使浏览器在当前这一帧中提前进行reflow,于是「布局颠簸」就产生了。
如果这种事情在一帧中大量发生,那么对性能的影响是巨大的。可以参考一个例子>>todo:fastdom/**/aspect.html;
上文中,我们了解到根据浏览器时间精度的不同,使用requestAnimationFrame所注册的一个异步任务(也就是回调函数)会在下一次浏览器检查异步任务队列的时候触发。
如果这个回调进行了DOM操作,由于浏览器刷新(/repaint/layout 随便怎么称呼)一次的时间间隔一般都是1000/60ms,因此DOM操作也将在「下一帧」才能生效!
于是我们可以改成这样:
1 | // Read |
在每一帧中,先执行所有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动画
在解决布局颠簸问题,并且缓存各种状态之后,我们还有以下可能提升性能的地方:
- 忽略过于微小甚至不可见的状态改变(<1px)
- 根据实际情况主动触发GPU硬件加速
- raf
css动画
上面虽然列举了一系列的问题,但css动画还是又快又好。
在2015 Google I/0 的官方站点中,
使用了一种被称作FLIP的原则开发动画效果链接,译文