如何优化动画??

1,855 阅读7分钟

动画基础知识

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出现的问题:

  1. 根据 event loop 的规则,setTimeout 会被推到"任务队列"挂起,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
  2. 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 动画卡顿的解决方案:

  1. 在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。
  2. 选择器越复杂,浏览器计算得越久。最糟情况下,浏览器需要遍历整个DOM-tree,计算量等于元素总个数乘以选择器个数。尽量不要使选择器太复杂,事先给需要被操作的元素加上类名。
  3. reflow总是牵涉整个文档流。修改元素css后立刻读取css计算值,将导致浏览器同步reflow,阻塞js线程。
  4. 使用 will-change 通知浏览器你打算更改元素的属性。浏览器会在你进行更改之前做最合适的优化。但不要过度使用 will-change,因为这样做会浪费浏览器资源,从而导致更多的性能问题。

相关动画插件

  • Echarts:不多说
  • Zrender:Echarts 是基于 Zrender 封装、加工实现的。Zrender 又是对 canvas 的封装。
  • motaion:蚂蚁对外的动画组件
  • three.js:3D 动画,基于 webGL 封装的 API
  • bodymovin:ui导出相应的文件,使用插件实现动画效果。
  • chart.js:与 Ecahrts 类似的插件

相关参考资料