sundway / blog

oh~~
110 stars 6 forks source link

渲染性能 #2

Open sundway opened 8 years ago

sundway commented 8 years ago

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

RAIL 性能模型

首先一个需要思考的问题,怎样的网站是顺畅的?我们可能可以给一个大概的感觉,如:秒级响应等。其实,也可以给出一个很讨巧的答案:用户觉得顺畅的网站它就是顺畅的。因为几乎所有网站都希望将用户留在页面上,当然以用户为中心建立性能模型是必要的。下面是 Google 提出的一个以用户为中心的性能模型,里面的数据不是 Google 首创,有一些论文做类似研究(如:100ms 响应用户是一个很合适的时间等)。

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

应用要达到上面的性能模型需要从哪些方面入手呢?如果我们知道浏览器是如何渲染一个页面的,并且去优化渲染过程中的关键步骤,是不是就能事半功倍呢?

关键渲染路径

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

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

优化JavaScript的执行

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

函数的输入事件处理

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

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

其他优化:

添加或移除一个 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 处理这些纹理的问题。

其他

jingjie1988 commented 8 years ago

666 mark 后面仔细看

iceprosurface commented 8 years ago

先mark不是很能理解,现在不是性能过剩么……

sundway commented 8 years ago

@iceprosurface 没有的,安卓机分配到浏览器的内存不多的,动画写几个触发布局的就会造成卡顿了。

fengmiaosen commented 8 years ago

文章说的一些方法已经在实际应用了

sundway commented 8 years ago

@fengmiaosen 写动画的时候我会比较注意。。。

fengzilong commented 8 years ago

CSSDOM -> CSSOM

sundway commented 8 years ago

@fengzilong 已修正。

fengzilong commented 8 years ago

@sundway :+1:

huang-xiao-jian commented 8 years ago

渲染层分布可以通过工具量化,如何判断渲染层收益大于付出,求指教。

sundway commented 8 years ago

@bornkiller 渲染层如何量化没有结论,优化的时候,分析工具利用 timeline 对比一下就好了。业务中一般动画元素做独立层不太会造成层爆炸,目前遇到最明显的优化还是避免触发 layout 。

yuanyuanlife commented 8 years ago

CSS 选择器的性能优化带来的好处远比不上提高的开发麻烦程度

sundway commented 8 years ago

@bokeyy 赞同,CSS 选择器的性能优化效果很有限,但开发成本比较高,如果不是专门做渲染性能是不会考虑的。

mlyknown commented 8 years ago

学习~~

kinglion commented 8 years ago

Mark~看到web worker跟requestAnimateFrame很新奇,以后估计可以用得上

sundway commented 8 years ago

@kinglion web worker的支持程度比较好,应用场景也很高,最近做一个3D动画,物理计算很复杂,如果不用 web worker 后台计算就搞不了了。

gaofant101 commented 7 years ago

mark,

huang-xiao-jian commented 7 years ago

是否可以这样理解: JavaScript执行DOM增删操作,会影响到DOM tree结构,从而调整render tree,然后应该才是样式计算及后续步骤。如果动态插入CSS样式表,应该也是调整render tree在先,UI同步在后。

sundway commented 7 years ago

@bornkiller 增删dom势必会影响整个渲染,https://csstriggers.com/ 每个属性的成本都不一样。

zhuzhuaicoding commented 7 years ago

mark

sundway commented 6 years ago

test

liangshuai commented 6 years ago

mark