浏览器渲染性能优化

渲染性能

页面不只要快速加载,并且要顺畅地运行;滚动应与手指的滑动同样快,而且动画和交互应如丝绸般顺滑。css

60fps 与设备刷新率

目前大多数设备的屏幕刷新率为 60 /。所以,若是在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也须要跟设备屏幕的刷新率保持一致。html

其中每一个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工做要作,所以全部工做须要在 10 毫秒内完成。若是没法符合此预算,帧率将降低,而且内容会在屏幕上抖动(judders)。此现象一般称为卡顿(jank,会对用户体验产生负面影响。jquery

像素管道(The pixel pipeline)git

在工做时须要了解并注意五个主要区域,这些是拥有最大控制权的部分,也是像素至屏幕管道中的关键点:github

 

  • JavaScript。通常来讲,会使用 JavaScript 来实现一些视觉变化的效果。好比用 jQuery 的 animate 函数作一个动画、对一个数据集进行排序或者往页面里添加一些 DOM 元素等。除了 JavaScript,还有其余一些经常使用方法也能够实现视觉变化效果,好比:CSS Animations、Transitions 和 Web Animation API。
  • 样式计算Sytle calculations。This is the process of figuring out which CSS rules apply to which elements based on matching selectors, for example, .headline or .nav > .nav__item. From there, once rules are known, they are applied and the final styles for each element are calculated.
  • 布局。在知道对一个元素应用哪些规则以后,浏览器便可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其余元素,例如 <body> 元素的宽度通常会影响其子元素的宽度以及树中各处的节点,所以对于浏览器来讲,布局过程是常常发生的。
  • 绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,包括元素的每一个可视部分。绘制通常是在多个表面(一般称为层layers)上完成的。
  • 合成。因为页面的各部分可能被绘制到多层,由此它们须要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另外一元素重叠的元素来讲,这点特别重要,由于一个错误可能使一个元素错误地出如今另外一个元素的上层。

管道的每一个部分都有机会产生卡顿,所以务必准确了解代码触发管道的哪些部分。浏览器

不必定每帧都老是会通过管道每一个部分的处理。实际上,无论是使用 JavaScript、CSS 仍是网络动画,在实现视觉变化时,管道针对指定帧的运行一般有三种方式:安全

1. JS / CSS > 样式 > 布局 > 绘制 > 合成网络

 

若是修改元素的“layout”属性,即改变了元素的几何属性(例如宽度、高度等),那么浏览器将必须检查全部其余元素,而后“自动重排”页面(reflow the page)。任何受影响的部分都须要从新绘制,并且最终绘制的元素需进行合成。app

2. JS / CSS > 样式 > 绘制 > 合成框架

 

若是修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制。

3. JS / CSS > 样式 > 合成

 

若是更改一个既不用从新布局也不要从新绘制的属性,则浏览器将只执行合成。这个最后的方式开销最小,最适合于应用生命周期中的高压力点,例如动画或滚动。

性能是一种避免执行的艺术,而且使执行的任何操做尽量高效。 许多状况下,这须要与浏览器配合,而不是跟它对着干。 值得谨记的是,上面列出的各项管道工做在计算开销上有所不一样,一些任务比其余任务的开销要大!

优化 JavaScript 执行

JavaScript often triggers visual changes. Sometimes that's directly through style manipulations, and sometimes it's calculations that result in visual changes, like searching or sorting data.时机不当或长时间运行的 JavaScript 多是致使性能问题的常见缘由,应当设法尽量减小其影响。

JavaScript 性能分析能够说是一门艺术,由于编写的 JavaScript 代码与实际执行的代码彻底不像。现代浏览器使用 JIT 编译器和各类各样的优化和技巧来实现尽量快的执行,这极大地改变了代码的动态性。

一些帮助应用很好地执行 JavaScript的事情:

  • 对于动画效果的实现,避免使用 setTimeout 或 setInterval,使用 requestAnimationFrame。
  • 将长时间运行的 JavaScript 从主线程移到 Web Worker。
  • 使用小任务来执行对多个帧的 DOM 更改。
  • 使用 Chrome DevTools 的 Timeline 和 JavaScript 分析器来评估 JavaScript 的影响。

使用 requestAnimationFrame 来实现视觉变化

当屏幕正在发生视觉变化时,最好在帧的开头执行操做。保证 JavaScript 在帧开始时运行的惟一方式是使用 requestAnimationFrame。

/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */
function updateScreen(time) {
  // Make visual updates here.
}
requestAnimationFrame(updateScreen);

框架或示例可能使用 setTimeout 或 setInterval 来执行动画之类的视觉变化,但这种作法的问题是,回调函数在帧中的某个时点运行,可能恰好在帧的末尾,而这常常会使咱们丢失帧,致使卡顿。(composite等js的运行须要时间,会阻塞UI更新)。

 

事实上,jQuery 目前的默认 animate 行为是使用 setTimeout!强烈建议打上补丁程序以使用 requestAnimationFrame

下降复杂性或使用 Web Worker

JavaScript 在浏览器的主线程上运行,刚好与样式计算、布局以及许多状况下的绘制一块儿运行。若是 JavaScript 运行时间过长,就会阻塞这些其余工做,可能致使帧丢失。

所以,要妥善处理 JavaScript 什么时候运行以及运行多久。例如,若是在滚动之类的动画中,最好是想办法使 JavaScript 保持在 3-4 毫秒的范围内。超过此范围,就可能要占用太多时间。若是在空闲期间,则能够没必要那么斤斤计较所占的时间。

在许多状况下,能够将纯计算工做移到 Web Worker,例如,不须要 DOM 访问权限,数据操做或遍历(例如排序或搜索),每每很适合这种模型,加载和模型生成也是如此。

var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// The main thread is now free to continue working on other things...
dataSortWorker.addEventListener('message', function(evt) {
   var sortedData = evt.data;
   // Update data on screen...
});

并不是全部工做都适合此模型:Web Worker 没有 DOM 访问权限。若是操做必须在主线程上执行,能够考虑一种批量方法,将大型任务分割为小任务,每一个小任务所占时间不超过几毫秒,而且在每帧的 requestAnimationFrame 处理程序内运行。

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
  var taskFinishTime;
  do {
    // Assume the next task is pushed onto a stack.
    var nextTask = taskList.pop();
    // Process nextTask.
    processTask(nextTask);
    // Go again if there’s enough time to do the next task.
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);
  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);
}

此方法会产生 UX 和 UI 后果,您将须要使用进度或活动指示器来确保用户知道任务正在被处理。在任何状况下,此方法都不会占用应用的主线程,从而有助于主线程始终对用户交互做出快速响应。

了解 JavaScript 的“frame tax”

在评估一个框架、库或本身的代码时,务必逐帧评估运行 JavaScript 代码的开销。当执行性能关键的动画工做(例如变换或滚动)时,这点尤为重要。

测量 JavaScript 开销和性能状况的最佳方法是使用 Chrome DevTools。一般,您将得到以下的简单记录:

 

The Main section provides a flame chart of JavaScript calls so you can analyze exactly which functions were called and how long each took.

若是发现有长时间运行的 JavaScript,则能够在 DevTools 用户界面的顶部启用 JavaScript 分析器:

 


以这种方式分析 JavaScript 会产生开销,所以必定只在想要更深刻了解 JavaScript 运行时特性时才启用它。启用此复选框后,如今能够执行相同的操做,您将得到有关 JavaScript 中调用了哪些函数的更多信息:

 

有了这些信息以后,就能够评估 JavaScript 对应用性能的影响,并开始找出和修正函数运行时间过长的热点(hotspots)。如前所述,应当设法移除长时间运行的 JavaScript,或者若不能移除,则将其移到 Web Worker 中,腾出主线程继续执行其余任务。

避免微优化 JavaScript

知道浏览器执行一个函数版本比另外一个函数要快 100 倍可能会很酷,好比请求元素的offsetTop比计算getBoundingClientRect()要快,可是,您在每帧调用这类函数的次数几乎老是不多。所以,把重点放在 JavaScript 性能的这个方面一般是白费劲。您通常只能节省零点几毫秒的时间。

若是您开发的是游戏或计算开销很大的应用,则可能属于本指南的例外状况,由于您通常会将大量计算放入单个帧,在这种状况下各类方法都颇有用。

简而言之,慎用微优化,由于它们一般不会映射到您正在构建的应用类型。2/8法则,先从瓶颈处着手优化。

缩小样式计算的范围并下降其复杂性

经过添加和删除元素,更改属性、类或经过动画来更改 DOM,都会致使浏览器从新计算元素样式,在不少状况下还会对页面或页面的一部分进行布局(即自动重排)。This process is called computed style calculation.

计算样式的第一部分是建立一组匹配选择器,这实质上是浏览器计算出给指定元素应用哪些classes, pseudo-selectors and IDs。

第二部分涉及从匹配选择器中获取全部样式规则,并计算出此元素的最终样式。在 Blink(Chrome 和 Opera 的渲染引擎)中,这些过程的开销至少在目前是大体相同的:

Roughly 50% of the time used to calculate the computed style for an element is used to match selectors,而另外一半时间用于从匹配的规则中构建 RenderStyle(computed style representation)。

  • 下降选择器的复杂性;使用以类为中心的方法,例如 BEM规范(Block-Element_Modifer)。
  • 减小必须计算其样式的元素数量。

下降选择器的复杂性

在最简单的状况下,在 CSS 中只有一个类的元素:

.title {
  /* styles */
}

可是,随着项目的增加,将可能产生更复杂的 CSS,最终的选择器可能变成这样:

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

为了知道是否须要应用样式,浏览器实际上必须询问“这是否为有 title 类的元素,其父元素刚好是负第 N 个子元素加上 1 个带 box 类的元素?”计算此结果可能须要大量时间,具体取决于所用的选择器和相应的浏览器。特定的选择器能够更改成一个类:

.final-box-title {
  /* styles */
}

开发者可能对该类的名称有疑问,但此工做对于浏览器而言要简单得多。在上一版本中,为了知道该元素是否为其类型的最后一个,浏览器首先必须知道关于其余元素的全部状况,以及其后面是否有任何元素会是第 N 个最后子元素,这比简单地将类选择器与元素匹配的开销要大得多。

生成render tree时,对于每一个DOM元素,必须在全部Style Rules中找到符合的 selector 并将对应的样式规则进行合并。
css选择器的解析是从右往左的,这样公共样式就在CSSOM树的父节点上,更具体的样式(选择器更具体)会在子节点上,节点分支和遍历次数都会变少。若是采用 left-to-right 的方式读取css规则,那么大多数规则读到最后才会发现是不匹配,作了不少无用功;而采起 right-to-left 的方式,只要发现最右边选择器不匹配,就直接舍弃,避免不少无效匹配。

减小要计算样式的元素数量

另外一个性能考虑,在元素更改时须要计算的工做量对于许多样式更新而言是更重要的因素。

In general terms, the worst case cost of calculating the computed style of elements is the number of elements multiplied by the selector count, because each element needs to be at least checked once against every style rule to see if it matches.

注:之前曾经是这样:若是改变了(例如)body 元素上的一个类,则该页的全部子元素将须要从新计算其计算样式。如今有点不同:对于更改时会致使从新计算样式的元素,某些浏览器维护一小组每一个这种元素独有的规则。这意味着,根据元素在树中的位置以及所改变的具体属性,元素不必定须要从新计算。

样式计算可能常常是直接针对少许目标元素,而不是声明整个页面无效。在现代浏览器中,这每每再也不是个问题,由于浏览器并不必定须要检查一项更改可能影响的全部元素。另外一方面,较早的浏览器不必定针对此类任务进行了优化。应当尽量减小声明为无效的元素的数量

注:若是您热衷于网页组件,有一点值得注意,样式计算在这方面稍有不一样,由于默认状况下样式不会跨越 Shadow DOM 的边界,而且范围限于单个组件,而不是整个树。可是,整体来看,一样的概念仍然适用:规则简单的小树比规则复杂的大树会获得更高效地处理。

测量样式从新计算的开销

测量样式从新计算的最简单、最好的方法是使用 Chrome DevTools 的 Timeline 模式。首先,打开 DevTools,转至 Timeline 选项卡,选中记录并与您的网站交互。中止记录后,将看到下图所示状况。

 

顶部的条表示每秒帧数,若是看到柱形超过较低的线,即 60fps 线,则存在长时间运行的帧。

 

若是一些滚动之类的交互或其余交互时出现长时间运行的帧,则应当进一步审查。

若是出现较大的紫色块,如上例所示,请点击记录了解到更多细节。

 

 

在此次抓取中,有一个长时间运行的从新计算样式事件,其时间恰好超过 18 毫秒,而且刚好发生在滚动期间,致使用户体验到明显的抖动。

若是点击事件自己,将看到一个调用栈,精确指出了您的 JavaScript 中致使触发样式更改的位置。此外,还得到样式受更改影响的元素数量(本例中恰好超过 400 个元素),以及执行样式计算所花的时间。您可使用此信息来开始尝试在代码中查找修正点。

使用BEM规范

BEM的编码方法实际上归入了上述选择器匹配的性能优点,由于它建议全部元素都有单个类,而且在须要层次结构时也归入了类的名称:

.list { }
.list__list-item { }

若是须要一些修饰符,像在上面咱们想为最后一个子元素作一些特别的东西,就能够按以下方式添加:

.list__list-item--last-child {}

若是您在寻找一种好方法来组织您的 CSS,则 BEM 真的是个很好的起点,不只从结构的角度如此,还由于样式查找获得了简化。

避免大型、复杂的布局和布局抖动

布局是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。 根据所用的 CSS、元素的内容或父级元素,每一个元素都将有显式或隐含的大小信息。此过程在 Chrome、Opera、Safari 和 Internet Explorer 中称为布局 (Layout)。 在 Firefox 中称为自动重排 (Reflow),但实际上其过程是同样的。

与样式计算类似,布局开销的直接考虑因素以下:

  1. 须要布局的元素数量。
  2. 这些布局的复杂性。
  • 布局的做用范围通常为整个文档。
  • DOM 元素的数量将影响性能,应尽量避免触发布局。
  • 评估布局模型的性能;新版 Flexbox比旧版 Flexbox 或基于浮动的布局模型更快。
  • Avoid forced synchronous layouts and layout thrashing; read style values then make style changes.

尽量避免布局操做

当更改样式时,浏览器会检查更改是否须要计算布局,以及是否须要更新渲染树。几何属性(如宽度、高度、左侧或顶部)的更改都须要布局计算。

.box {
  width: 20px;
  height: 20px;
}
/** Changing width and height triggers layout. */
.box--expanded {
  width: 200px;
  height: 350px;
}

布局几乎老是做用到整个文档。 若是有大量元素,将须要很长时间来算出全部元素的位置和尺寸。

若是没法避免布局,关键仍是要使用 Chrome DevTools 来查看布局要花多长时间,并肯定布局是不是形成瓶颈的缘由。首先,打开 DevTools,选择“Timeline”标签,点击“record”按钮,而后与您的网站交互。当您中止记录时,将看到网站表现状况的详细分析:

 

在仔细研究上例中的框架时,咱们看到超过 20 毫秒用在布局上,当咱们在动画中设置 16 毫秒来获取屏幕上的帧时,此布局时间太长。您还能够看到,DevTools 将说明树的大小(本例中为 1618 个元素)以及须要布局的节点数。

使用 flexbox 而不是较早的布局模型

网页有各类布局模型,一些模式比其余模式受到更普遍的支持。最先的 CSS 布局模型使咱们可以在屏幕上对元素进行相对、绝对定位或经过浮动元素定位。

下面的屏幕截图显示了在 1,300 个框上使用浮动的布局开销。固然,这是一我的为的例子,由于大多数应用将使用各类手段来定位元素。

 

 

若是咱们更新此示例以使用 Flexbox(Web 平台的新模型),则出现不一样的状况:

 

如今,对于相同数量的元素和相同的视觉外观,布局的时间要少得多(本例中为分别 3.5 毫秒和 14 毫秒)。务必记住,对于某些状况,可能没法选择 Flexbox,由于它没有浮动那么受支持,可是在可能的状况下,至少应研究布局模型对网站性能的影响,而且采用最大程度减小网页执行开销的模型。

在任何状况下,无论是否选择 Flexbox,都应当在应用的高压力点期间尝试彻底避免触发布局

避免强制同步布局

将一帧送到屏幕会采用以下顺序:

 

首先 JavaScript 运行,而后计算样式,而后布局。可是,JavaScript 在更改元素样式后,获取其几何属性的值,此时会强制浏览器应用新样式提早执行布局,值后才能获取几何属性值。这被称为强制同步布局(forced synchronous layout

要记住的第一件事是,在 JavaScript 运行时,来自上一帧的全部旧布局值是已知的,而且可供您查询。所以,若是(例如)您要在帧的开头写出一个元素(让咱们称其为“框”)的高度,可能编写一些以下代码:

// Schedule our function to run at the start of the frame.
requestAnimationFrame(logBoxHeight);
function logBoxHeight() {
  // Gets the height of the box in pixels and logs it out.
  console.log(box.offsetHeight);
}

若是在请求此框的高度以前,已更改其样式,就会出现问题:

function logBoxHeight() {
  box.classList.add('super-big');    //样式更改后,浏览器必须先应用新的样式(重绘)以后才能获取当前的值,有时是多作无用功
  // Gets the height of the box in pixels and logs it out.
  console.log(box.offsetHeight);
}

如今,为了得到框的高度,浏览器必须先应用样式更改(因为增长了 super-big 类),而后运行布局,这时才能返回正确的高度。这是没必要要的,而且可能开销很大。

所以,始终应先批量读取样式并执行(浏览器可使用上一帧的布局值),而后执行任何赋值操做。

以上函数应为:

function logBoxHeight() {
  // Gets the height of the box in pixels and logs it out.
  console.log(box.offsetHeight);
  box.classList.add('super-big');
}

大部分状况下,并不须要先应用新样式而后查询值,使用上一帧的值就足够了。与浏览器同步(或比其提早)运行样式计算和布局可能成为瓶颈。

避免布局超负荷(thrashing)

有一种方式会使强制同步布局更糟:连续执行大量这种强制布局。以下:

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

此代码循环处理一组段落,并设置每一个段落的宽度以匹配一个称为“box”的元素的宽度。这看起来没有害处,但问题是循环的每次迭代读取一个样式值 (box.offsetWidth),而后当即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,由于 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,它会自动批处理读取和写入,应当能防止意外触发强制同步布局或布局抖动。

简化绘制的复杂度、减少绘制区域

绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它每每是管道中运行时间最长的任务,应尽量避免此任务。

  • 除 transform 或 opacity 属性以外,更改任何属性始终都会触发绘制。
  • 绘制一般是像素管道中开销最大的部分,应尽量避免绘制。
  • 经过layer promotion和动画的编排来减小绘制区域。
  • 使用 Chrome DevTools paint profile来评估绘制的复杂性和开销;应尽量下降复杂性并减小开销。

触发布局与绘制

若是触发布局,则老是会触发绘制,由于更改任何元素的几何属性意味着其像素须要修正!

 

若是更改非几何属性,例如背景、文本或阴影,也可能触发绘制。在这些状况下,不须要布局,而且管道将以下所示:

 

使用 Chrome DevTools 快速肯定绘制瓶颈

 

您可使用 Chrome DevTools 来快速肯定正在绘制的区域。打开 DevTools,按下键盘上的 Esc 键。在出现的面板中,转到“rendering”标签,而后选中“Show paint rectangles”。

打开此选项后,每次发生绘制时,Chrome 将让屏幕闪烁绿色。若是看到整个屏幕闪烁绿色,或看到不该绘制的屏幕区域,则应当进一步研究。

 

Chrome DevTools Timeline 中有一个选项提供更多信息:绘制分析器。要启用此选项,转至 Timeline,而后选中顶部的“Paint”框。须要注意的是,请务必仅在尝试分析绘制问题时才打开此选项,由于它会产生开销,而且会影响性能分析结果。最好是在想要更深刻了解具体绘制内容时使用。

 

完成了上述设置以后,如今能够运行 Timeline 录制,而且绘制记录将包含更多的细节。经过点击一帧的绘制记录,您将进入该帧的绘制分析器:

点击绘制分析器将调出一个视图,您能够查看所绘制的元素、所花的时间,以及所需的各个绘制调用:

 

此分析器显示区域和复杂性(实际上就是绘制所花的时间),若是不能选择避免绘制,这两个都是能够设法修正的方面。

提高移动或淡出的元素

绘制并不是老是绘制到内存中的单个图像。事实上,在必要时浏览器能够绘制到多个图像或合成层(compositor layers)。

 

此方法的优势是,按期重绘的或经过变形在屏幕上移动的元素,能够在不影响其余元素的状况下进行处理。Sketch、GIMP 或 Photoshop 之类的艺术文件也是如此,各个层能够在彼此的上面处理并合成,以建立最终图像。

建立新层的最佳方式是使用 will-change CSS 属性。此方法在 Chrome、Opera 和 Firefox 上有效,而且经过 transform 的值将建立一个新的合成器层:

.moving-element {
  will-change: transform;
}

对于不支持 will-change 但受益于层建立的浏览器,例如 Safari 和 Mobile Safari,须要使用3D 变形来强制建立一个新层:

.moving-element {
  transform: translateZ(0);
}

但须要注意的是:不要建立太多层,由于每层都须要内存和管理开销。

若是已将一个元素提高到一个新层,可以使用 DevTools 确认这样作已带来性能优点。请勿在不分析的状况下提高元素。

减小绘制区域

然而有时,虽然提高元素,却仍须要绘制工做。绘制问题的一个大挑战是,浏览器将两个须要绘制的区域联合在一块儿,而这可能致使整个屏幕重绘。所以,若是页面顶层有一个固定标头,而在屏幕底部还有正在绘制的元素,则整个屏幕可能最终要重绘。

减小绘制区域每每是编排动画和变换,使其不过多重叠,或设法避免对页面的某些部分设置动画。

下降绘制的复杂性

 

在谈到绘制时,一些绘制比其余绘制的开销更大。例如,绘制任何涉及模糊(例如阴影)的元素所花的时间将比(例如)绘制一个红框的时间要长。可是,对于 CSS 而言,这点并不老是很明显:background: red; 和 box-shadow: 0, 4px, 4px, rgba(0,0,0,0.5); 看起来不必定有大相径庭的性能特性,但确实很不相同。

利用上述绘制分析器,您能够肯定是否须要寻求其余方式来实现效果。问问本身,是否可能使用一组开销更小的样式或替代方式来实现最终结果。

您要尽量的避免绘制的发生,特别是在动画效果中。由于每帧 10 毫秒的时间预算通常来讲是不足以完成绘制工做的,尤为是在移动设备上。

相关文章
相关标签/搜索