1. 为什么卡顿

浏览器渲染没有处理好的情况下,经常会出现卡顿的情况,尤其是动画效果,可能会出现一顿一顿的情况。这种情况是怎么来的呢?

为了更好的说明这个问题,这里先说明显示器的刷新频率。这个概念很简单,简单来说就是,每一秒钟显示器画面刷新的次数,也就是每一秒钟显示器能显示多少副图像。这个概念在我们平时的生活中非常常见,比如我们看电影的时候,我们看到的其实是一副一副静止的画面,只是画面切换的间隔很短,我们观察不出来。这里画面之间的切换就是我们所说的画面刷新。同理我们的电视,电脑显示器也是一样的原来。只是电视,显示器的刷新频率要高一些,一般为60Hz。也就是说我们的显示器每一秒钟更新60次画面。

每一秒钟刷新60次意味着什么呢?也就是说每一帧之间的间隔时间为16毫秒多一点(1s/60 = 16.66ms)。也就意味着原理上浏览器有16.66ms完成每一帧绘制工作。但对于我们来说,这个时间会进一步缩短,因为浏览器本身还需要做许多其他额外的工作,所以留给我们完成所有的工作的时间一般只有10ms。如果你的工作没有在这个时间内完成,那么浏览器就无法绘制出这些帧,也就就无法显示在显示器上,从而出现卡顿。

2. 浏览器渲染一帧的几个关键步骤

现在知道为什么会出现卡顿,那么如何避免卡顿呢?按照上面的原理,我们需要做的就是在尽量在短的时间内完成每一帧的绘制,所以问题的关键就是如何在规定时间内完成每一帧的绘制。怎样做到这一点呢?首先需要了解浏览器渲染一帧的几个关键节点,尽量减少这几个节点的耗时,从而解决卡顿的问题。

比如我们有一个动画,它不断修改(通过Js或者css)一个元素的样式。那浏览器如何绘制出下一帧呢,是怎样的过程呢?通常来说有三个步骤:

  1. 重排(reflow)
    什么是重排呢?换个说法可能更容易理解一些,重排其实就是重新布局。我们知道浏览器获取到站点数据之后,会先解析HTML生成DOM树(dom tree),然后解析CSS生成样式规则树(style rule tree),然后解析Javascript(Js可以修改DOM tree和Style rule treee)。最终形成一颗渲染树(render tree)。在第一次渲染出这棵渲染树的时候,就有会产生第一次布局,之后修改元素的某些样式,就会再一次进行布局,也就是重排。重排的主要工作就是:浏览器根据元素的样式规则,计算出元素占用空间的大小以及它在屏幕上的位置。重排是一个很耗费浏览器性能的操作,因为一旦触发重排,浏览器都要重新计算每个元素的形状和位置。修改元素大小(width, height, font-size等)和位置(position)一般都会触发浏览器的重排,这里大家可能对position这个触发重绘有点不好理解,按照平时我们熟悉的css的流的概念,position:absolute,在另外一层上应该不会影响文档流才对。但这里我们只考虑了一个方面,它虽然不影响文档流,但它却依赖文档流,我们的绝对定位依赖直接父级的位置属性,所以当然要进行重排了。要判断属性是否会触发重排主要也看这两点:是否影响文档流,是否依赖文档流。按照这个规则我们很容易判断其他的样式属性是否会引起重排,比如position:fixed,按照我们的规则,就不会引起重排。具体有哪些样式会触发重排可以查看这里

  2. 重绘(repaint)
    重绘也很容易理解,就是重新绘制的意思。主要就是一个填充像素的过程,它涉及文本,颜色,阴影,图像等。就是浏览器在知道元素的大小和位置之后,按照给定的文本,颜色,阴影,图像等将元素绘制出来。比如color,background等这些属性的修改,就会触发浏览器的重绘。绘制一般是在多个图层上进行的,和PS里图层的概念差不多。

  3. 合成(composite)

    正如上面所说的页面的各个部分是绘制在多个图层上的,最后会有一个合并渲染层的过程,就是将它们按照正确的顺序绘制到屏幕上,最终显示在我们的显示屏上。

当然不是每一次都需要执行这三个步骤,你可以跳过其中的一些步骤。但你执行其中一个步骤,后面的肯定会执行,意思就是你触发了重排,就一定会产生重绘和合成。触发了重绘就会发生合并,合并也是一定会发生的。所以为了我们能在规定时间内完成绘制,我们就需要尽可能的跳过或是减少重排和重绘的过程,减少这些对性能影响很大的计算。那么如何尽量减少呢?其实浏览器内部本身就针对这些做了很多优化,比如浏览器就会将reflow和repaint的操作累计一些之后再做composite。我们就应该尽量减少一些操作强制触发浏览的reflow。具体有哪些操作会强制触发reflow,可以参考这里。下面有一些常规的建议。

  • 尽量减少使用js对dom进行一条一条的样式修改,可以使用修改classname的方法。
  • 批量的对DOM进行修改,不要循环一次一次的修改。
  • 尽量不要使用table进行布局。
  • 动画可以尽量用transform和opacity实现,这两个属性的修改之后引起composite。详细可以查看css triggerFLIP原则

    比起前两个过程,合并过程耗费浏览器的性能要小得多,。现代浏览器还可以使用GPU(图形处理器)来加速这一过程。如何来开启GPU加速呢?其实就是为元素添加一些规则将其提升为独立的层,具体有:

    • 使用3D和透视变换(transform,perspective)。
    • 拥有3D或2D上下文的元素(WebGL,canvas,video等)。
    • 混合插件(flash)。
    • 2D变换和透明度样式(transform,opactiy)。
    • 使用will-change属性。
    • 使用css过滤器(filter)。
    • 元素A有一个 z-index 比自己小的元素B,且元素B是一个合成层(换句话说就是该元素在复合层上面渲染),则元素A会提升为合成层。

    GPU擅长大规模的并行计算,也就是我们说的人海战术,处理图像数据非常酷快,所以使用GPU进行图层的合并会使你的动画效果变得非常流畅,而且相对独立于CPU,JavaScript计算对对图像产生的影响就很小,也就是说即时在动画过程中,你还有大量的js计算,动画也依然能保持流畅。这样看来使用GPU的方案非常完美,那我是不是任何元素都单独出一个图层,这样是不是出图也快呢?当然不是这样的,了解一下GPU你就知道,GPU只有很少的cache,而且图层数据需要传给GPU,就像我们的网络请求一样。而我们每一个