/ 性能优化,浏览器

浏览器渲染优化

60fps和设备刷新率

当今大多数设备的屏幕刷新率都是 60次/秒 。因此,如果在页面中有一个动画或渐变效果,或者用户正在滑动页面,那么浏览器渲染动画或页面的每一帧的速率,也需要跟设备屏幕的刷新率保持一致。
也就是说,浏览器对每一帧画面的渲染工作需要在16毫秒(1秒 / 60 = 16.66毫秒)之内完成。但实际上,在渲染某一帧画面的同时,浏览器还有一些额外的工作要做(比如渲染队列的管理,渲染线程与其他线程之间的切换等等)。因此单纯的渲染工作,一般需要控制在10毫秒之内完成,才能达到流畅的视觉效果。如果超过了这个时间限度,页面的渲染就会出现卡顿效果,也就是常说的jank,它是很糟糕的用户体验。

像素渲染流水线

在编写web页面时,你需要理解你所写的页面代码是如何被转换成屏幕上显示的像素的。这个转换过程可以归纳为这样的一个流水线,包含五个关键步骤:

  • JavaScript。一般来说,我们会使用JavaScript来实现一些视觉变化的效果。比如用jQuery的animate函数做一个动画、对一个数据集进行排序、或者往页面里添加一些DOM元素等。当然,除了JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations, Transitions和Web Animation API。
  • 计算样式。这个过程是根据CSS选择器,比如.headline或.nav > .nav_item,对每个DOM元素匹配对应的CSS样式。这一步结束之后,就确定了每个DOM元素上该应用什么CSS样式规则。
  • 布局。上一步确定了每个DOM元素的样式规则,这一步就是具体计算每个DOM元素最终在屏幕上显示的大小和位置。web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。
  • 绘制。绘制,本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
  • 渲染层合并。由上一步可知,对页面中DOM元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
    上述过程的每一步中都有发生jank的可能,因此一定要弄清楚你的代码将会运行在哪一步。

Note
你可能听说过 "rasterize" 这个术语,它通常被用在绘制过程中。绘制过程本身包含两步: :1)创建一系列draw调用;2)填充像素。 第二步的过程被称作 "rasterization" 。因此当你在DevTools中查看页面的paint记录时,你可以认为它已经包含了 rasterization。(有些浏览器会使用不同的线程来完成这两步,不过这也不是web开发者能控制的了)

降低样式选择器的复杂度

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

使用transform/opacity实现动画效果

从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做渲染层的合并即可:
为了实现上述效果,你需要对元素谨慎使用会被修改的样式属性,只能使用那些仅触发渲染层合并的属性。目前,只有两个属性是满足这个条件的:transforms和opacity.

避免大规模、复杂的布局

尽可能避免触发布局

当你修改了元素的样式属性之后,浏览器会将会检查为了使这个修改生效是否需要重新计算布局以及更新渲染树。对于DOM元素的“几何属性”的修改,比如width/height/left/top等,都需要重新计算布局。

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

/**
 * Changing width and height
 * triggers layout.
 */
.box--expanded {
  width: 200px;
  height: 350px;
}
使用flexbox替代老的布局模型

Google给的实例中,对页面中1300个盒对象使用浮动布局的时间消耗为14.289毫秒,而对这些元素使用Flexbox的布局方式时间消耗为3.544毫秒

避免强制同步布局事件的发生

如果你在读取height属性之前,修改了box的样式,那么可能就会有问题了.

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属性值。但其实我们可以避免这个不必要且耗费昂贵的布局过程。

为了避免触发不必要的布局过程,你应该首先批量读取元素样式属性(浏览器将直接返回上一帧的样式属性值),然后再对样式属性进行写操作。

上面的JavaScript函数的正确写法应该是:

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';
  }
}

这段代码对一组段落标签执行循环操作,设置

标签的width属性值,使其与box元素的宽度相同。看上去这段代码是OK的,但问题在于,在每次循环中,都读取了box元素的一个样式属性值,然后立即使用该值来更新

元素的width属性。在下一次循环中读取box元素offsetwidth属性的时候,浏览器必须先使得上一次循环中的样式更新操作生效,也就是执行布局过程,然后才能响应本次循环中的样式读取操作。也就意味着,布局过程将在_每次循环_中发生。

我们使用_先读后写_的原则,来修复上述代码中的问题:

var width = box.offsetWidth;

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

转载自google