动画基础知识
1. 动画帧率(FPS)计算
帧的定义:1幅画就叫做“1帧”,每秒帧数指的就是“每秒播放的画面数”。每一帧都是静止的图象,快速连续地显示帧便形成了运动的假象。高的帧率可以得到更流畅、更逼真的动画。每秒钟帧数 (fps) 愈多,所显示的动作就会愈流畅。
理论上说,FPS 越高,动画会越流畅,目前大多数设备的屏幕刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果最好,也就是每帧的消耗时间为 16.67ms。
直观感受,不同帧率的体验:
- 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
- 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
- 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
- 帧率波动很大的动画,亦会使人感觉到卡顿。
计算帧率的方法:
- 借助 Chrome 开发者工具 - FPS meter
- 使用 requestAnimationFrame 计算 FPS 原理,算法总有6种,详情请看 -》blog.csdn.net/allen807733…
2. setTimeout 与 requestAnimationFrame
setTimeout出现的问题:
- 根据 event loop 的规则,setTimeout 会被推到"任务队列"挂起,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
- HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。
例子:
var FPS = 60;
setTimeout(draw, 1000/FPS);
上述代码,如果draw带有大量逻辑计算,导致计算时间超过帧等待时间时,将会出现丢帧。除外,如果FPS太高,超过了当时浏览器的重绘频率,将会造成计算浪费,例如浏览器实际才重绘2帧,但却计算了3帧,那么有1帧的计算就浪费了。
引入requestAnimationFrame,这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,以满足开发者操作动画的需求。
var fps = 30;
var now;
var then = Date.now();
var interval = 1000/fps;
var delta;
function tick() {
requestAnimationFrame(tick);
now = Date.now();
delta = now - then;
if (delta > interval) {
// 这里不能简单then=now,否则还会出现上边简单做法的细微时间差问题。例如fps=10,每帧100ms,而现在每16ms(60fps)执行一次draw。16*7=112>100,需要7次才实际绘制一次。这个情况下,实际10帧需要112*10=1120ms>1000ms才绘制完成。
then = now - (delta % interval);
draw(); // ... Code for Drawing the Frame ...
}
}
tick();
3. transition 与 animate
transition:一般用来做过渡的, 没时间轴的概念, 通过事件触发(一次),没中间状态(只有开始和结束)
animate:做动效,有时间轴的概念(帧可控),可以重复触发和有中间状态;
过渡的开销比动效小,前者一般用于交互居多,后者用于活动页居多;
如何优化动画
大部分网站性能优化可以对动画有一定的优化作用,在这里不累赘地细讲了,详情请看 -》网站性能优化实战:juejin.cn/post/684490…
本文主要讲动画的为什么会出现动画卡顿问题,针对这种问题该如何解决。
1. 为什么会造成动画卡顿呢?
Blink 内核早期架构
以 Chrome 浏览器内核 Blink 渲染页面为例。对早期的 Chrome 浏览器而言,每个页面 Tab 对应一个独立的 renderer 进程,Renderer 进程中包含了主线程和合成线程。早期 Chrome 内核架构:
其中,主线程主要负责:
- Javascript 的计算与执行
- CSS 样式计算
- Layout 计算
- 将页面元素绘制成位图(paint),也就是光栅化(Raster)
- 将位图给合成线程
合成线程则主要负责:
- 通过GPU,将位图绘制到屏幕上
- 对可见或即将可见的区域,询问主线程是否进行位图更新。
- 计算页面的可见区域
- 当滚动屏幕时,计算出即将可见的区域
- 当滚动时移动页面区域
主线程和合成线程的调度不合理会造成渲染出现卡顿等问题。
2. CSS 动画与 JS 动画的细微区别
FPS(JS) = Time(主线程) + Time(合成线程)
FPS(CSS) = Time(合成线程)(有时候,例如opacity, transform)
在不频繁触发主线程时,css 动画比 js 动画节省性能。
如果任何动画触发了绘制,布局,或者两者,那么「主线程」会来完成该工作。这个对基于 CSS 还是 JavaScript 实现的动画都一样,布局或者绘制的开销巨大,让与之关联的 CSS 或 JavaScript 执行工作、渲染都变得毫无意义。
3. CSS 动画卡顿性能优化
例子:
transition:margin 2s;
在使用height,width,margin,padding作为transition的值时,会造成浏览器主线程的工作量较重,例如从margin-left:-20px渲染到margin-left:0,主线程需要计算样式margin-left:-19px,margin-left:-18px,一直到margin-left:0,而且每一次主线程计算样式后,合成进程都需要绘制到GPU然后再渲染到屏幕上,前后总共进行20次主线程渲染,20次合成线程渲染,20+20次,总计40次计算。
主线程的渲染流程,可以参考浏览器渲染网页的流程:
- 使用 HTML 创建文档对象模型(DOM)
- 使用 CSS 创建 CSS 对象模型(CSSOM)
- 基于 DOM 和 CSSOM 执行脚本(Scripts)
- 合并 DOM 和 CSSOM 形成渲染树(Render Tree)
- 使用渲染树布局(Layout)所有元素
- 渲染(Paint)所有元素
也就是说,主线程每次都需要执行Scripts,Render Tree ,Layout和Paint这四个阶段的计算。
transition:transform 2s;
而如果使用transform的话,例如tranform:translate(-20px,0)到transform:translate(0,0),主线程只需要进行一次tranform:translate(-20px,0)到transform:translate(0,0),然后合成线程去一次将-20px转换到0px,这样的话,总计1+20计算。
css 动画卡顿的解决方案:
- 在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。
- 选择器越复杂,浏览器计算得越久。最糟情况下,浏览器需要遍历整个DOM-tree,计算量等于元素总个数乘以选择器个数。尽量不要使选择器太复杂,事先给需要被操作的元素加上类名。
- reflow总是牵涉整个文档流。修改元素css后立刻读取css计算值,将导致浏览器同步reflow,阻塞js线程。
- 使用 will-change 通知浏览器你打算更改元素的属性。浏览器会在你进行更改之前做最合适的优化。但不要过度使用 will-change,因为这样做会浪费浏览器资源,从而导致更多的性能问题。
相关动画插件
- Echarts:不多说
- Zrender:Echarts 是基于 Zrender 封装、加工实现的。Zrender 又是对 canvas 的封装。
- motaion:蚂蚁对外的动画组件
- three.js:3D 动画,基于 webGL 封装的 API
- bodymovin:ui导出相应的文件,使用插件实现动画效果。
- chart.js:与 Ecahrts 类似的插件
相关参考资料
- Web 动画帧率(FPS)计算:www.cnblogs.com/coco1s/p/80…
- requestAnimationFrame播放动画:www.cnblogs.com/kenkofox/p/…
- 什么是 Event Loop?:www.cnblogs.com/Nelsen8/p/7…
- CSS3动画卡顿性能优化解决方案:segmentfault.com/a/119000001…
- 深入浏览器理解CSS animations 和 transitions的性能问题:sy-tang.github.io/2014/05/14/…
- 动画优化:www.cnblogs.com/liuhao-web/…
- JavaScript 是如何工作的:juejin.cn/post/684490…