从浏览器渲染原理说一说如何实现高效的动画
写在前面在平时的工作中,页面的动画效果是很常见的需求。那么,怎么样实现一个高效的动画呢? 本文首发于公众号:符合预期的CoyPan注,本文谈到的浏览器,均为基于Chromium的现代浏览器。 页面渲染原理 一个页面展示在用户面前,简单来说,会经历以上5个步骤。我们可以把上面这个图称为像素管道。 Javascript: 执行js逻辑,修改DOM,修改CSS等。Style:计算样式。Layout:在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。这个步骤,就是我们常说的重排。Paint: 绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。这个步骤,就是我们常说的重绘。Composite:渲染层合并,由上一步可知,对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。在浏览器中,页面的渲染由浏览器的渲染进程完成,而渲染进程中,包含了主线程,worker线程,Compositer线程,Raster线程。上述像素管道的5个过程中,前4个过程,都由主线程完成,最后一个步骤,主要由Raster线程、Compositer线程完成。 JavaScript、Style、Layout像素管道中的前三个步骤,大家都很熟悉了。JavaScript、Style两个步骤,一图以蔽之: 接着是Layout,浏览器遍历render tree的每一个节点,计算其确切大小和位置。最终形成一个Layout Tree。 Paint在Paint之前,浏览器会根据Layout Tree,确定需要绘制的对象的层级,我们可以把这个层级叫做渲染层,最终生成Layer Tree。这个阶段被称作:Update Layer Tree 在Paint这个阶段,浏览器会根据Layer Tree,生成Paint Records。 Paint Records就是描述先画什么,再画什么的记录,跟我们写canvas代码时很像。Paint Records是根据渲染层划分的的。来看一个Paint Records的实例: 尽管生成了Pain Records,真正的绘制并不在Paint这个阶段完成的,而是在Composite阶段由Raster线程完成的。 Composite经过之前的几个步骤,浏览器主线程已经将页面的内容分成了若干渲染层。为了提升性能,某些特定的渲染层,会被提升为合成层。我们可以通过下面两个css属性,将某个元素强制提升为合成层: will-change: transform;// 或者transform: translateZ(0);注:提升为合成层的条件比较复杂,这里就不一一展开了。可以参考这篇文章:http://taobaofed.org/blog/201... 主线程在处理完所有的所有的数据后,会把数据提交到Compositer线程。Composite线程会利用Raster线程来做光栅化处理,并将处理好的内容存入内存中。随着Composite线程完成渲染层合成操作,扔给GPU,页面最终被渲染到屏幕上。 可以通过Chrome开发者工具中的Layer来查看合成层: 其他运行方式的像素管道上文中的像素管道共有5个步骤。不一定每帧都总是会经过管道每个部分的处理。实际上,不管是使用 JavaScript、CSS 还是网络动画,在实现视觉变化时,管道针对指定帧的运行还有其他两种方式: 第一种就是我们所说的页面没有进行重排,只进行了重绘;第二种就是页面既没有进行重排,也没有进行重绘。 最后这种运行方式的开销最小,适合于页面上的动画效果。 实现动画效果不考虑canvas等,有三种常见的方式来实现页面上的动效, 完全不用css3相关属性,仅使用setTimeout, setInterval, requestAnimationFrame,通过js修改DOM的样式来实现动画。使用纯css3来实现动画。js与css3相结合来实现动画。一般情况下,使用第一种方式的时候,虽然有的动画效果在进行过程中不会触发像素管道中的Layout,但是Paint往往是避免不了的。而使用css3来实现动画时,我们可以跳过Layout和Paint步骤。 下面,来看看三种实现方式下,浏览器的处理过程。 完全由JS驱动的动画代码如下: <html> <head> <style type="text/css"> #test2 { margin-top: 100px; width: 100px; height: 100px; position: relative; background-color: black; } </style> </head> <body> <p> 这是一段无用的文字,这是一段无用的文字,这是一段无用的文字,这是一段无用的文字,这是一段无用的文字,这是一段无用的文字 </p> <div id="test2"></div> <script type="text/javascript"> window.onload = function() { const el = document.getElementById('test2'); let left = 0; const startTimeStamp = Date.now(); const fn = function() { left += 2; if(Date.now() - startTimeStamp > 2000) { return; } el.style.left = left + 'px'; return window.requestAnimationFrame(fn); } window.requestAnimationFrame(fn) } </script> </body></html>选取动画过程中的一帧,浏览器的处理过程如下: ...