渲染性能分析(下)

上篇咱们大体分析了在处理JavaScript阶段和Style阶段须要注意的问题,这篇咱们就来看下在Layout、Paint、Composite阶段以及处理用户行为的时候,应该关注的问题所在。css

避免大型的复杂的布局和布局限制

Layout阶段浏览器将计算元素的大小,在页面中的位置,其余元素的影响等等,与样式计算(Style calculation)相似,基本限制因素以下:git

  • 须要Layout的元素数量
  • Layout的复杂度

TL;DRgithub

  • Layout适用于整个文档流
  • DOM的数量直接影响Layout的性能消耗,尽可能避免触发Layout
  • 避免强制同步修改Layout,形成反复Layout。即读取style的值而后修改style

尽量的避免触发Layout

当更改样式时,浏览器会去检查需不需从新计算触发Layout,通常来讲修改元素的几何属性(geometric properties)例如:宽高,布局定位都会触发Layout浏览器

.box {
  width: 200px;
  height: 200px;
}

// 改变元素宽高 触发Layout
.box-expanded: {
  width: 300px;
  height: 300px;
}

Layout是做用于全局整个文档流的,因此若是有大量的元素须要处理,就会消耗很长时间去计算这些元素的大小和定位。
若是没法避免触发Layout,能够经过Performance查看Layout阶段的耗时是不是影响性能的瓶颈。缓存

Snipaste_2019-10-23_17-24-29-4fe35133-c763-4d7a-90ac-84483eb9262c

在Performance中咱们能够清楚的看到Layout阶段消耗的时间,以及涉及的节点数(如图为314个元素)
https://csstriggers.com/ 列出了一些CSS属性会触发渲染的哪一个阶段,能够做为对照参考。
另外使用flexbox布局要比传统的经过float或者相对定位绝对定位实现布局更快。工具

避免出现强制同步布局

正常状况下渲染步骤是先执行JavaScript,而后是style calculation 而后触发Layout。可是有种状况是触发Layout的时间点早于JavaScript的执行,这种状况叫强制同步布局(forced synchoronous layout)布局

要明确的是在JavaScript运行时,前一帧的布局属性值都是已知的。举个例子来讲若是你想在帧(frame)开始前获取某个元素的高度,就能够这样写:性能

requestAnimtionFrame(logBoxHeight);

function logBoxHeight(){
  console.log(element.offsetHeight);
}

可是若是你先改变的元素的样式而后在获取元素高就会出问题flex

function logBoxHeight(){
  element.classList.add('big');
  console.log(element.offsetHeight);
}

如今的状况就变成这样,因为添加了新的class后要输入元素的offsetHeight,浏览器必须先从新进行布局计算才能拿到正确的offsetHeight的值,这彻底是不必的,并且这个例子中一般状况下都是不须要先去设置样式再去取属性值的,直接使用最后一帧的属性值彻底足够了。因此通常状况下最好是先去读取须要的属性值,而后再作更改。优化

function logBoxHeight(){
  console.log(element.offsetHeight);
  element.classList.add('big');
}

还有一种更糟糕的状况是反复不断的强制同步触发layout。看下面的代码

function resizeAllParagraphsToMatchBlockWidth(){
    // 让浏览器陷入读写循环
  for(let i = 0; i < paragraphs.length; i++){
    paragraphs[i].style.width = element.offsetHeight + 'px';
  }
}

打眼一看好像没什么问题,其实这种问题很常见每次迭代都会去读取element.offsetHeight属性,而后用它去更新paragraph的width属性。解决办法也很常见就是读取一次作一个缓存。

const width = element.offsetHeight;

function resizeAllparagraphsToMatchBlockWidth(){
    for(let i = 0; i < paragraphs.length;i++){
        paragraphs[i].style.width = width + 'px';
    }
}

简化Paint复杂度,减小Paint的面积

Paint是一个填充像素(pixels)的过程,最终这些像素会经过合成器合成到屏幕。这个阶段一般是渲染元素整个过程当中最消耗时间的阶段,因此要尽量的避免

TL;DR

  • 除了transform和opacity属性改变其余任何属性都会触发Paint
  • 由于Paint在整个渲染过程当中是最消耗时间和性能的,因此尽量的避免触发
  • 利用Chrome DevTools来观察Paint阶段,并尽量的下降减少对性能的消耗
  • 能够经过提高图层来减小Paint的面积大小

若是触发Layout确定触发Paint,由于改变元素的几何属性(宽高等)意味着须要从新布局定位。固然修改一些非几何属性例如:background text-color,shadow这些也会触发paint,只不过不会触发layout因此整个渲染过程就会跳过Layout阶段。
Snipaste_2019-10-24_19-17-20-a9b6b4bf-10d0-43b9-a72f-095c69237835

利用Chrome DevTools来观察渲染过程当中最消耗性能的部分,能够看到以下图绿色部分表示的是须要被重绘的区域。
Snipaste_2019-10-24_19-26-52-e3d8d020-8732-4f82-a60f-779cdef8f440

可使用will-change属性或者相似的hack手段让浏览器建立一个新的图层来减小须要被Paint的区域。关于will-change的详细内容能够看这篇文章【关于will-change属性你须要知道的事】此处不在赘述。

尽量简化Paint的过程,在Paint阶段有的步骤是很是消耗性能的,好比任何涉及到模糊(blur)的过程(例如:shadow属性),就CSS而言这些属性之间看上去没什么性能上的差别,但实际在Paint阶段是区别仍是很明显的。

Composite

composite阶段是将Paint过程当中的内容聚集起来显示在屏幕上。

这个过程当中主要有两个影响页面性能的关键因素:一个是须要整合的合成层(compoaitor layers)数量,另外一个是用于动画的相关属性

TL;DR

  • 使用will-change或translateZ属性作硬件加速
  • 避免建立过多图层(layer),图层会占用内存
  • 对于动画的操做使用transform和opacity作变动

渲染过程当中最好的状况是避免触发Layout和Paint只须要合成(compositing)阶段处理变动。要作到这一点只须要一直使用只经过合成器处理的属性便可。(只有transform和opacity属性能够作到)

Postion   transform: translate(npx,npx);
Scale     transform: scale(n);
Rotation  transform: rotate(ndeg);
Skew      transform: skew(X|Y)(ndeg);
Matrix    transform: matrix(3d)(...);
Opacity   opacity: 0 <= n <= 1

使用transform和opacity的注意点是对应元素要在自身的合成图层,若是没有自身图层就要建立一个图层。这里涉及到建立图层和硬件加速的内容能够参考【关于will-change属性你须要知道的事

经过提高或建立图层有助于性能的提高这个技巧诱惑力是大的,因此有可能就会写出以下代码:

* {
    will-change: transform;
    transform: translateZ(0);
}

如同在 【关于will-change属性你须要知道的事】里提到的这种作法非但不能带来性能上的提高,反而会占用过多系统资源,对CPU和GPU都会带来额外的负担。

最后和咱们以前提到的相似Chrome DevTools提供了供开发者查看页面图层的工具,能够看到当前页面上有多少层级,每一个层级的大小、渲染的次数以及合成的缘由等等,咱们能够经过这些信息去分析和作对应的优化。
Snipaste_2019-10-24_22-54-43-39b79651-7d26-493d-ae0a-74384cc8fbf2

对输入处理程序作防抖

处理用户输入也是潜在的可能会影响性能的因素,由于其可能会阻塞其余内容的加载而且致使没必要要的布局(layout)工做

TL;DR

  • 避免时间过长运行处理输入程序,其会阻塞页面滚动;
  • 不要在处理输入的程序中修改样式;
  • 对输入处理程序作防抖,在下一帧的requestAnimationFrame回调中存储事件值和样式的更改

避免运行时间过长的处理程序

页面交互最快的状况是,当用户与页面交互时,页面的合成器线程接受用户的触摸输入并将内容四处移动。这个过程不须要与主线程通讯,而是直接提交给GPU处理。因此不须要等待主线程对JS的处理、以及布局(layout)、绘制(paint)等操做完成。

Snipaste_2019-10-21_17-27-22-296a06f0-6a55-41a7-af01-ff773c8e1f7c

可是,若是附加了输入处理程序(如touchstart,touchmove,或者touchend)后,合成器线程必须等待该处理程序执行完毕,由于有可能调用了preventDefault()来阻止触摸滚动事件的发生。即便没有调用preventDefault(),合成器也必须等待其执行完毕,这样用户的滚动操做就被阻止就可能致使帧丢失从而引发卡顿。
Snipaste_2019-10-21_17-28-10-7182cacd-7819-4f15-8957-594510110f12

总而言之,你应该确保运行的全部输入处理程序都快速执行,并容许合成器执行其工做。

避免在输入处理程序中改变样式

输入处理程序被安排在requestAnimtionFrame回调以前运行。若是在这个处理程序中作样式上的修改,那么在requestAnimationFrame开始处有须要更改的样式处理,这会触发强制同步布局。

Snipaste_2019-10-21_17-41-01-f4a205b3-b672-4a2c-9ab0-e21e97d1d2d5

输入处理程序作防抖

上面两个问题的解决方案是相同的:你应该对下一个requestAnimationFrame的回调中作样式更改的状况作防抖处理。

function onScroll(evt){
    lastScrollY = window.scrollY;

    if(scheduleAnimationFrame)
        retun;
    scheduleAnimationFrame = true;
    requestAnimationFrame(readAndUpdatePage);
}

window.addEventListener('scroll',onScroll);

这样作还有一个好处,就是保持输入处理程序的轻量,由于这样就不会阻塞好比滚动等其余操做。

原文:渲染性能分析(下)

相关文章
相关标签/搜索