说到web的高性能动画,这部份内容其实已是老生常谈的了,不过其中仍是有很多比较新的并且很是实用的内容能够和你们分享一下。 读完这篇文章后相信你们都会对动画渲染的机制以及制做60fps动画的关键要素有足够的理解,之后赶上了动画相关的问题也能够很好的从源头上解决。javascript
动画帧率能够做为衡量标准,通常来讲画面在 60fps 的帧率下效果比较好。 css
换算一下就是,每一帧要在 16.7ms (16.7 = 1000/60) 内完成渲染。所以,咱们的首要任务是减小没必要要的性能消耗。 越多的帧须要渲染的,意味着有越多的任务须要浏览器处理,因此掉帧就出现了,这是达到 60fps 的一个绊脚石。html
若是全部动画都没法在 16.7ms 渲染完毕,不如考虑用略低的 30fps 帧率来渲染。html5
这里主要决定因素有二:java
时机(Frame Timing): 新的一帧准备好的时机git
成本(Frame Budget): 渲染新的一帧须要多长的时间github
通常来讲咱们使用setTimeout(callback, 1/60)
来实现16.7ms后执行动画一帧的渲染。 然而setTimeout
实际上并不许确。 首先,setTimeout
依靠浏览器内置时钟的更新频率 例如:IE8及之前更新间隔为15.6ms,setTimeout(callback, 1/60)
为16.7ms,那么它就须要两个15.6ms才会触发,这也意味着无端延迟了 15.6 x 2 - 16.7 = 14.5毫秒。 web
其次,假使可以达到16.7ms,它还要面临一个异步队列的问题。 由于异步的关系setTimeout
中的回调函数并不是当即执行,而是须要加入等待队列中。但问题是,若是在等待延迟触发的过程当中,有新的同步脚本须要执行,那么同步脚本不会排在timer的回调以后,而是当即执行。浏览器
function runForSeconds(s) {
var start = +new Date();
while (start + s * 1000 > (+new Date())) {}
}
document.body.addEventListener("click", function () {
runForSeconds(10);
}, false);
setTimeout(function () {
console.log("Done!");
}, 1000 * 3);
复制代码
以上的例子是,若是在等待触发延迟的3秒过程当中,有人点击了body,那么回调仍是准时在3s完成时触发吗? 实践执行的时候,它会等待10s,同步函数老是优先于异步函数。性能优化
基于这些问题咱们提出了另外一个解决方案:requestAnimationFrame(callback)
window.requestAnimationFrame() 方法告诉浏览器您但愿执行动画并请求浏览器在下一次重绘以前调用指定的函数来更新动画。该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。-- MDN
当咱们调用这个函数的时候,咱们告诉它须要作两件事:
与 setTimeout 相比,rAF(requestAnimationFrame) 最大的优点是由系统来决定回调函数的执行时机。
具体一点讲就是,系统每次绘制以前会主动调用 rAF 中的回调函数,若是系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,若是绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。
换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次(函数节流,这篇文章就不细说了,感兴趣的能够查一下),这样就不会引发丢帧现象,也不会致使动画出现卡顿的问题。
另外它能够自动调节频率。若是callback工做太多没法在一帧内完成会自动下降为30fps。虽然下降了,但总比掉帧好。
同时对比使用 setTimeout 实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,因为此时页面处于不可见或不可用状态,刷新动画是没有意义的,并且还浪费 CPU 资源。而 rAF 则彻底不一样,当页面处理未激活的状态下,该页面的屏幕绘制任务也会被系统暂停,所以跟着系统步伐走的 rAF 也会中止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。
对于rAF的兼容性问题其实已经有了很好的处理方案了,如下是一种比较简单的:
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
复制代码
这种写法没有考虑 cancelAnimationFrame 的兼容性,而且不是全部的设备绘制时间间隔都是1000/60。
这个是比较不错的polyfil 。
总的来讲,rAF解决了前面的第一个问题(绘制时机),至于第二个问题(绘制成本),rAF是无能为力的,最多也就是采起自动下降频率的方式处理。
这里就须要从浏览器渲染方面来优化了,首先看下这个图:
页面首次加载时,浏览器会下载并解析 HTML,将 HTML 元素转变为一个 DOM 节点的「内容树」(content tree)。除此以外,样式一样会被解析生成「渲染树」 (render tree)。为了提高性能,渲染引擎会分开完成这些工做,甚至会出现渲染树比 DOM 树更快生成出来。
在这个阶段里最影响绘制时间的天然就是Layout了
// animation loop
function update(timestamp) {
for(var m = 0; m < movers.length; m++) {
// DEMO 版本
//movers[m].style.left = ((Math.sin(movers[m].offsetTop + timestamp/1000)+1) * 500) + 'px';
// FIXED 版本
movers[m].style.left = ((Math.sin(m + timestamp/1000)+1) * 500) + 'px';
}
rAF(update);
};
rAF(update);
复制代码
上面例子里DEMO版本是很是慢的,之因此慢的缘由是,在修改每个物体的left值时,会请求这个物体的offsetTop值,触发了重排,这是一个很是耗时的reflow操做。
一般咱们会不知不觉中写了不少的频繁layout的代码,例如:
var h1 = element1.clientHeight;
element1.style.height = (h1 * 2) + 'px';
var h2 = element2.clientHeight;
element2.style.height = (h2 * 2) + 'px';
var h3 = element3.clientHeight;
element3.style.height = (h3 * 2) + 'px';
复制代码
不断地读写 DOM 会致使「强制同步布局」(forced synchronous layouts),不过在技术发展过程当中它演变成了更形象的词 — 「布局抖动」(layout thrashing)(详情能够看一下这篇文章 layout thrashing)。 浏览器会追踪「脏元素」,在合适的时候将变换过程储存起来,而后在读取了特定属性之后,开发者能够强制浏览器提早计算,这样反复的读写会致使重排。 因此这里咱们须要进行优化,先读后写就是一个解决方案,上面的代码能够改写为:
// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
复制代码
固然这种只能应对一些普通的状况,若是代码是解耦的或者更复杂的读写后嵌套读写操做的这些状况可使用一些比较成熟的解决方案,例如fastdom.js。另一个小技巧是使用rAF来延迟所有的写操做到下一帧执行也是很不错的解决方案。
生成布局后,浏览器将页面绘制到屏幕上。这个环节和前一个步骤相似,浏览器会追踪脏元素,将它们合并到一个超大的矩形区域中。每一帧内只会发生一次重绘,用于绘制这个被污染区域。
这个阶段对性能的影响主要在于重绘。
减小没必要的绘制
例如,gif图即便不可见,也可能致使paint,不须要时应将gif图的display属性设为none 在常常paint的区域,要避免代价过高的style 代价比较高的样式:
color,border-style,visibility,background,
text-decoration,background-image,
background-position,background-repeat
outline-color,outline,outline-style
border-radius,outline-width,box-shadow
background-size
复制代码
参考网站:csstriggers.com/
减小绘制的区域
为引发大范围Paint的元素生成独立的Layer以减少Paint的范围
能够参考一下这个demo网站,绿色部分为重绘区域:
将全部绘制好的元素进行复合。 默认状况下,全部元素将会被绘制到同一个层中,若是将元素分开到不一样的复合层中,更新元素对性能友好,不在同一层的元素不容易受到影响。
这一阶段里CPU 绘制层,GPU 生成层。GPU 复合层上的改变代价最小性能消耗最少。因此这里的优化主要就是把代价高的改动都放到GPU上,也就是通常说的开启硬件加速技术,能够说有益无害,若是设备的性能足够开启就对了。
这里的限制主要有:GPC和CPU之间带宽,GPU的限度。
这里须要区分一下CPU,GPU的工做:
合成线程则主要负责:
而GPU就只须要绘制图层了,因此硬件加速的性能无疑更好。
开启硬件加速的方式主要有:
opacity
和 transform
的值触发transform
的3D属性强制开启GPU加速will-change
显式地通知浏览器对某一个元素的某个或某些元素作渲染优化硬件加速以后,浏览器会为此元素单首创建一个“层”。当有单独的层以后,此元素的repaint操做将只须要更新本身,不用影响到别人。你能够将其理解为局部更新。因此开启了硬件加速的动画会变得流畅不少
默认状况下,transform
、opacity
这类css属性CPU是直接通知GPU来作处理的,由于GPU能快速对texture(纹理:CPU传输到GPU的一个Bitmap)进行偏移、缩放、旋转、修改透明度等操做,不通过主线程的layout、paint过程。也就是开启了硬件加速。
will-change
是个新事物,它可以显式地通知浏览器对某一个元素的某个或某些元素作渲染优化。 will-change
接收各类各样的属性值,好比一个或多个 CSS 属性 (transform
, opacity
)、contents
或者 scroll-position
。不过最经常使用值可能就是 auto
,这个值表示的是浏览器将进行默认的优化:
GPU虽然擅长处理图像,可是它也有瓶颈。
链接CPU和GPU之间的带宽是有限的,若是一次更新的层太多,则很容易就达到GPU的瓶颈,影响到动画的流畅度。因此咱们须要控制层的数量和层paint的次数。
控制层的数量能够理解,由于层的建立和更新都会消耗内存。而控制层paint的次数,是为了减小位图更新的次数。每次位图更新,合成线程就须要提交新的位图给GPU。频繁地更新位图也会拖慢GPU的效率。
优化有度,咱们总能听到关于「复合层过多反而阻碍渲染」的讨论。由于浏览器已经为优化作了能作的一切, will-change 的性能优化方案自己对资源要求很高。若是浏览器持续在执行某个元素的 will-change,就意味着浏览器要持续对这个元素的进行优化,性能消耗形成页面卡顿。
过多的复合层下降页面性能的现象在移动端很常见。
避免意外生成的layer
z-index高于Layer的元素,也会生成单独的Layer
实现丝般顺滑主要决定因素有二:
时机(Frame Timing):
成本(Frame Budget):
避免layout:先读后写
尽可能少paint:注意样式的使用
适当的硬件加速