渲染性能

该文章于三天前发表在 github,如有问题可提至 github.css

这篇文章主要关注的是资源加载以后的性能,由于大多数用户关注的不是应用如何加载而是具体的使用。因此要快速响应用户,尤为是无线端,咱们有必要了解浏览器渲染性能。git

RAIL 性能模型

首先一个须要思考的问题,怎样的网站是顺畅的?咱们可能能够给一个大概的感受,如:秒级响应等。其实,也能够给出一个很讨巧的答案:用户以为顺畅的网站它就是顺畅的。由于几乎全部网站都但愿将用户留在页面上,固然以用户为中心创建性能模型是必要的。下面是 Google 提出的一个以用户为中心的性能模型,里面的数据不是 Google 独创,有一些论文作相似研究(如:100ms 响应用户是一个很合适的时间等)。github

上图是 RAIL 的具体含义,这里有一些关键性的数据指标:web

  • Respond:0 - 100ms,视窗通常须要在这个时间段响应用户,超过这个时间段,用户就会感受到延时。chrome

  • Animation:0~16ms,屏幕每秒刷新60次,16ms 表明的是每一帧的时间。用户是很是关注动画的,当动画失帧很容易引发用户察觉。因此动画通常要控制在60FPS。浏览器

  • Idle:最大化主进程的空闲时间,这样能够及时响应用户输入。安全

  • Load:内容须要在1000ms 内加载出来,超过1000ms 会以为加载缓慢。性能优化

应用要达到上面的性能模型须要从哪些方面入手呢?若是咱们知道浏览器是如何渲染一个页面的,而且去优化渲染过程当中的关键步骤,是否是就能事半功倍呢?网络

关键渲染路径

上图是浏览器渲染的关键路径,首先,让咱们从浏览器解析一个页面开始吧。异步

  • 转化: 浏览器从磁盘或网络读取 HTML 的原始字节,浏览器会将这段原始文件按照相应编码规范进行解码(如今通常为 utf-8)。

  • 符号化:根据 W3C 标准转化为对应的符号(通常在尖括号内)。

  • DOM 构建:HTML 解析器会解析其中的 tag 标签,生成 token ,遇到 CSS 或 JS 会发送相应请求。HTML 解析时阻塞主进程的,CSS 通常也是阻塞主进程的(媒体查询时例外),也就是说它们在解析过程当中是没法作出响应的。而 JS 手动添加 async 后达到异步加载,根据 token 生成相应 DOM 树。

  • CSSDOM 构建,添加 CSS 样式生成 CSSDOM 树。

  • 渲染树构建,从 DOM 树的根节点开始,遍历每一个可见的节点,给每一个可见节点找到相应匹配的 CSSOM 规则,并应用这些规则,连带其内容及计算的样式。

  • 样式计算,浏览器会将全部的相对位置转换成绝对位置等一系列的样式计算。

  • 布局,浏览器将元素进行定位、布局。

  • 绘制,绘制元素样式,颜色、背景、大小、边框等。

  • 合成,将各层合成到一块儿、显示在屏幕上。

若是咱们是作一个动画,通常会用 JS 更改相应样式,接着浏览器就会经历 JS 运行、样式计算、布局、绘制、合成等多个重要步骤(后面还会讲到这个步骤实际过程当中能够更长或者更短)。那么要作的优化就是在这几个步骤中进行优化而且尽可能去掉中间的耗时步骤。

优化JavaScript的执行

上图描述的四个场景都是有可能对响应用户输入或者动画形成影响的。函数的输入事件处理、不合时机的 JS 、长时间的 JS 运行以及垃圾回收。

函数的输入事件处理

首先,咱们要知道的一个事实就是浏览器是由多个处理进程的:Compositor、Tile Worker、Main。当用户进行输入操做(滚动、点击等),如滚动时,Compositor 进程会接收到这个事件(实际它能够接受任何用户输入事件),若是能够的话,它将不会通知主进程,直接说:滚吧,牛宝宝。因而,页面就滚动了。固然,这其中包含更新层定位以及让 GPU 绘制帧,而主线程处于空闲状态。可是,事情每每并不是如此。若是输入事件上绑定了 JS 处理事件的话,Compositor 进程就没办法主动跳过主进程了。

如上图,当 JS 处理事件过长时,输入事件的响应会一直处于阻塞状态,直到 JS 处理完成。当响应超过 100ms 时,用户就会感觉到延时。因此当处理用户事件时,咱们应该作到:

  • 避免长时间的 JS 执行。

  • 避免在处理中改变样式。由于样式改变会引发后面布局、绘制、合成等操做。

  • 对用户输入进行消抖。

优化处理

其余优化:

  • 使用 requestAnimationFrame,将 setTimeout 换成 requestAnimationFrame,由于 setTimeout 时间控制可能形成在一帧的中间,目前各浏览器对 requestAnimationFrame 的支持已经比较好了。

  • 使用 Web Workers,将复杂计算的 JS 采用 Web Workers 进行处理。

  • 减小垃圾回收,垃圾回收是一个容易被忽略的问题,由于垃圾回收的时间是不受控制的,它可能在一个动画的中途,阻塞动画的执行,更理想的状况是在循环中复用对象。

样式计算

添加或移除一个 DOM 元素、修改元素属性和样式类、应用动画效果等操做,都会引发 DOM 结构的改变,从而致使浏览器须要从新计算每一个元素的样式、对页面或其一部分从新布局(多数状况下)。
计算样式的第一步是建立一套匹配的样式选择器,浏览器就是靠它们来对一个元素应用样式的。第二步是根据匹配的样式选择器来获取对应的具体样式规则,计算出最终具体有哪些样式是要应用在 DOM 元素上的。因此样式的优化也是这两步:

减少选择器的复杂性

如何减少选择器的复杂性?

.box:nth-last-child(-n+1) .title {
  /* styles */
}
.final-box-title {
  /* styles */
}

上面代码都是选择同一个元素,当元素不少时,第二个选择器的性能会明显优于第一个。BEM 规范有作相似事情,按照特性直接由一个选择器选择元素的性能每每会更优。

减小样式的计算量

由于元素的计算量和被改变的元素的数量成正比,因此你只须要注意一点,减小无效元素。

<div>
  <div>
    <p>多层无心义的标签</p>
  </div>
</div>

像上面的例子,有时候建立了一些冗余的标签。当改变外层的样式时,冗余的标签也须要进行样式计算,浪费性能。

布局

浏览器计算 DOM 元素的几何信息的过程:元素大小和在页面中的位置。每一个元素都有一个显式或隐式的大小信息,决定于其 CSS 属性的设置、或是元素自己内容的大小、或者是其父元素的大小。在 Blink/WebKit 内核的浏览器和 IE 中,这个过程称为 Layout。在基于 Gecko 的浏览器(好比 Firefox)中,这个过程称为 Reflow。

避免触发布局

目前,transform 和 opacity 只会引发合成,不会引发布局和从新绘制。整个流程中比较耗费性能的布局和绘制流程将直接跳过,性能显然是很好的。其余的 CSS 属性改变引发的流程会有所不一样,有些属性也会跳过布局,具体能够查看 CSS Triggers。因此,优化的第一步就是尽量避免触发布局。

使用Flexbox布局

Flexbox 布局方案性能会优于之前的布局方案,并且目前浏览器对 Flexbox 支持度至关高了:

避免强制同步布局事件

首先是执行 JS 脚本,而后是样式计算,而后是布局。可是,咱们还能够强制浏览器在执行 JS 脚本以前先执行布局过程,这就是所谓的强制同步布局。在 JS 脚本运行的时候,它能获取到的元素样式属性值都是上一帧画面的,都是旧的值。所以,若是你想在这一帧开始的时候,读取一个元素的 height 属性,你能够会写出这样的 JS 代码:

function logBoxHeight() {

  box.classList.add('super-big');

  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);
}

为了给你返回 box 的 height 属性值,浏览器必须首先应用 box 的属性修改(由于对其添加了 super-big 样式),接着执行布局过程。在这以后,浏览器才能返回正确的 height 属性值。这样就形成了同步布局事件,是很是消耗性能的。大多数状况下,你应该都不须要先修改而后再读取元素的样式属性值,使用上一帧的值就足够了。过早地同步执行样式计算和布局是潜在的页面性能的瓶颈之一。

function logBoxHeight() {
  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

避免快速连续的布局

还有一种状况比强制同步布局更糟:连续快速的屡次执行它。

function resizeAllParagraphsToMatchBlockWidth() {

  // Puts the browser into a read-write-read-write cycle.
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

上述代码对一组段落标签执行循环操做,设置 p 标签的width属性值,使其与 box 元素的宽度相同。看上去这段代码是没问题的,但问题在于,在每次循环中,都读取了 box 元素的一个样式属性值,而后当即使用该值来更新 p 元素的 widt h属性。在下一次循环中读取 box 元素 offsetwidth 属性的时候,浏览器必须先使得上一次循环中的样式更新操做生效,也就是执行布局过程,而后才能响应本次循环中的样式读取操做。布局过程将在每次循环中发生。优化代码:

// Read.
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = width + 'px';
  }
}

若是你想确保编写的读写操做是安全的,你可使用 FastDOM。它能帮你自动完成读写操做的批处理,还能避免意外地触发强制同步布局或快速连续的布局。

绘制

提高移动或渐变元素的绘制层

绘制并不是老是在内存中的单层画面里完成的。实际上,浏览器在必要时将会把一帧画面绘制成多层画面,而后将这若干层画面合并成一张图片显示到屏幕上。经过渲染层提高能够减少绘制区域,咱们能够用调试工具查看到绘制层:

在页面中新建一个渲染层最好的方式就是使用 will-change 属性,同时再与 transform 属性一块儿使用,就会建立一个新的组合层:

.element {
  will-change: transform;
}

对于那些目前还不支持 will-change 属性、但支持建立渲染层的浏览器,可使用一个 3D transform 属性来强制浏览器建立一个新的渲染层:

.element {
  transform: translateZ(0);
}

注意: 别盲目建立渲染层,必定要分析其实际性能表现。由于建立渲染层是有代价的,每建立一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。而且在移动端 GPU 和 CPU 的带宽有限制,建立的渲染层过多时,合成也会消耗跟多的时间。

仔细规划动画和简化绘制的复杂度

有时候,尽管把元素提高到了一个单独的渲染层,浏览器会把两个相邻区域的渲染任务合并在一块儿进行,这将致使整个屏幕区域都会被绘制。因此可使用调试工具查看,仔细规划动画。
不一样的 CSS 属性绘制的成本是不同的,绘制一个阴影就比绘制边框更费时。固然,这个浏览器也在不停优化中,如今的耗时渲染属性随时均可能被改变,因此须要多关注一下。

合成

渲染层的合并,就是把页面中完成了绘制过程的部分合并成一层,而后显示在屏幕上。下面和合成相关的两点前面也有提到过。

使用transform/opacity实现动画效果

前面已经提到过 transform/opacity 的优点,应用了 transforms/opacity 属性的元素必须独占一个渲染层。为了对这个元素建立一个自有的渲染层,你必须提高该元素。

管理渲染层、避免过多数量的层

建立一个新的渲染层须要消耗额外的内存和管理资源。而在内存资源有限的设备上,因为过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。因为每一个渲染层的纹理都须要上传到 GPU 处理,所以咱们还须要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。

其余

  • 关注趋势,今天不少的性能瓶颈极可能在未来都再也不是问题。如以前关注的一项技术 Web Animations,是否能用 JS 达到原生动画效果。Houdini,你能够添加更多的 JS 代码到动画中而不用担忧性能问题。

  • 利用工具 Chrome DevTools
    ,上面的规则只是优化的方向,善于利用工具分析。移动端利用 inspector 也是很是方便的,而且还能够对数据进行保存,对比分析等。几乎一切须要的分析工具,DevTools 都有。

  • 不要进行微优化,有时花上很短的带来的性能提高却很小,对于平常快速迭代的业务是不必这样作的。

相关连接

  • Paul Lewis 相关文章,Chrome 开发团队,旨在帮助开发者提升他们的应用和站点。

  • Performance Calendar,从2009年到2015年,列出了每年值得关注的优化方案。

  • Performance,Google Developers 官方站点对性能优化的文章。

  • webkit,webkit 的博客站点,介绍了比较底层的渲染等文章。

相关文章
相关标签/搜索