前端产品直接影响用户的体验,而用户体验上,操作的流畅性对其有着至关重要的影响。也许在PC上我们还感觉不出来强烈的卡顿,但是随着前端移动端的发展,Web 性能问题在移动端得到了急剧放大。什么才是真正的流畅呢?目前来说,与设备刷新频率保持一致,即 60fps 才可以说是真正的操作很流畅。这也是为什么大部分移动端 App 产品会选择使用原生的开发语言而非 H5 开发的一个重要原因。要知道,相对于 H5 开发,原生开发的代价高、开发效率低,而且很难做到App出了问题立马就能更新到用户端。所以,我们需要对我们的前端产品进行优化,尽量达到 60fps 的帧率,也就是说每一帧的运行时间最好不要超过 16ms。

前端性能优化是一个很大的话题,雅虎前端优化军规从宏观上告诉我们有哪些点是可以优化的,本文主要从其中的一个点,即浏览器端渲染这个微观角度上来介绍,更具体点来说,本文将结合 Chrome 调试工具来介绍常见的可优化点。

浏览器渲染基本流程

首先,需要大致地了解浏览器端渲染一个页面的基本流程。

当浏览器接收到服务器端的页面内容之后,它需要对整个 HTML 结构进行解析,形成 DOM 树;与此同时,它还需要对相应的 CSS 文件进行解析,形成 CSS 树(CSSOM)。接下来,需要结合 DOM + CSSOM,形成一个绘制树(Render Tree)。Render 树与 DOM 很像,区别在于,它不会包含 DOM 中的 head 等与渲染内容无关的结点,也不会包含 CSS 中定义为不可见的结点(对于 CSS 中定义的伪元素,如果可见,也会出现在绘制树中)。

得到绘制树之后,需要计算每个结点在页面中的位置,这一个过程称为layout(也有的称为reflow)。值得注意的是,layout 是一个计算代价相对很大的过程,它需要从根节点进行遍历,对每个可出现在页面中的节点进行位置的计算。

由于layout的过程中,我们是在一个连续的二维平面上进行的,接下来,需要将这些结果栅格化,映射到屏幕的离散二维平面上,这一过程称为 paint。paint实际上包含了两个任务:绘制(调用底层类似于Canvas的绘制API) + 栅格化(由composite线程控制,将绘制结果上传到GPU用于组合拼装页面)。Chrome调试窗口中,实心绿色框表示绘制,空心绿色框表示的是栅格化。现代浏览器为提升性能,将页面划分多个 layer,各自进行 paint 然后组合成一个页面(composite layers),可以与 PhotoShop 中的图层概念进行类比。这样做有什么好处呢?这样我们可以充分地利用 GPU 的并行处理能力,将某些 layer 传送到 GPU中进行绘制,即一系列的像素集合,并将结果传送到 CPU 中组装成一个页面。

这样我们得到的就是首次页面绘制的过程了。接下来,每一帧的过程中,又会发生什么事情呢?如下:

JavaScript => Style => Layout => Paint => Composite

每一帧中,如果 JS 动态地修改了 DOM 或 CSSOM,就会引起样式的更新,从而引起页面的 re-Layout,然后重新绘制结果并重新组装 layer。需要说明的是,并不是所有的修改都会引起上述所有的更新。如我们修改背景颜色的时候,就不会引起 Layout 的更新;但我们如果修改 width 等布局属性,则会引起上面的变化;而如果修改 transform: translate3d 这种硬件加速属性,Layout和Paint都不会更新,这会大大加速页面的性能。关于什么属性引起哪些更新,可以参考 CSS TRIGGERS

这也说明,在使用JavaScript进行动画的过程中,我们应该尽早在每一帧的开始执行 JS,这也是为什么推荐使用 requestAnimationFrame 的原因,因为它在每一帧刷新的时候都会调用,而且确保是每一帧最开始执行。

从另一个角度来看,页面生存周期内的状态及其用户可接受时间可以大致分为:加载数据(<1000ms)、响应(<100ms)、空闲期(50ms)、动画期(<16ms)。显然,我们的优化重点应该是在动画期,对于加载响应数据的优化,可以参考雅虎优化准则。接下来,我们就把我们的重点放在动画期的性能优化上,利用Chrome调试工具找出那些引起页面卡顿(Jank)的罪魁祸首。

性能优化

chrome-debugging-timeline-view

使用 Chrome Timeline 视图,我们可以清晰地看到每一帧执行的时间,具体发生了哪些事情,有哪些引起单帧时间过长。关于具体如何使用Timeline,这里不做介绍,毕竟它是一个实践性的东西。这里列举一下一些常见的性能问题:

  1. JavaScript 代码中出现的计算密集型任务,引起单帧时间太长。例如图像处理、对万级别的数据进行冒泡排序,显然它会导致页面直接卡顿,体验很差。这种情况下,一方面我们需要优化算法,另一方面,可以考虑使用 WebWorker 在另一个线程中进行计算。可以参考:http://www.html5rocks.com/en/tutorials/workers/basics/
  2. CSS样式的改变(不管是JS代码中改变的还是通过CSS3动画改变的):

反复读写的例子,这种情况下我们应该先把读操作缓存起来

for (var i = 0; i < paras.length; i++) {
    var width = block.offsetWidth;
    paras[i].style.width = width + ‘px’;
}

推荐课程: