目录
概述
卡顿现象
大多数设备的屏幕刷新率为 60 次/秒,浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。每帧预算时间仅比 16 毫秒多一点,由于浏览器还有准备工作要做。必须在10毫秒内完成,如果无法符合预期,就会出现屏幕抖动,我们成为卡顿。
像素管道 - 像素至屏幕管道中的关键点
- JavaScript。一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如用 jQuery 的 animate 函数做一个动画、对一个数据集进行排序或者往页面里添加一些 DOM 元素等。当然,除了 JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如: CSS Animations、Transitions 和 Web Animation API。
- 样式计算。此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
- 布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。
- 绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。
- 合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。
绘制实际上分为两个任务
- 创建绘图调用的列表
- 填充像素
第二步称为“栅格化 rasterize”
不一定每帧都总是会经过管道每个部分的处理。实际上,不管是使用 JavaScript、CSS 还是网络动画,在实现视觉变化时,管道针对指定帧的运行通常有三种方式:
1. JS / CSS > 样式 > 布局 > 绘制 > 合成
如果您修改元素的“layout”属性,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排(reflow)”页面。任何受影响的部分都需要重新绘制 (repainted),而且最终绘制的元素需进行合成
2. jS / CSS > 样式 > 绘制 > 合成
如果您修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制
3. JS / CSS > 样式 > 合成
如果您更改一个既不要布局也不要绘制的属性,则浏览器将跳到只执行合成。
这个最后的版本开销最小,最适合于应用生命周期中的高压力点,例如动画或滚动。
Note: 如果想知道更改任何指定 CSS 属性将触发上述三个版本中的哪一个,请查看 CSS 触发器。如果要快速了解高性能动画,请阅读更改仅合成器的属性部分。
优化 JavaScript 执行
JavaScript 经常会触发视觉变化。有时是直接通过样式操作,有时是会产生视觉变化的计算,例如搜索数据或将其排序。时机不当或长时间运行的 JavaScript 可能是导致性能问题的常见原因。您应当设法尽可能减少其影响。
更好好地执行 JavaScript
- 对于动画效果的实现,避免使用 setTimeout 或 setInterval,请使用requestAnimationFrame。
- 将长时间( long-running)运行的 JavaScript 从主线程移到 Web Worker。
- 使用微任务(micro-tasks)来执行对多个帧的 DOM 更改。
- 使用 Chrome DevTools 的 Timeline 和 JavaScript 分析器来评估 JavaScript 的影响。
使用 requestAnimationFrame 来实现视觉变化
当屏幕正在发生视觉变化时,您希望在适合浏览器的时间执行您的工作,也就是正好在帧的开头。保证 JavaScript 在帧开始时运行的唯一方式是使用 requestAnimationFrame。
function updateScreen(time) {
}
requestAnimationFrame(updateScreen);
框架或示例可能使用 setTimeout 或 setInterval 来执行动画之类的视觉变化,但这种做法的问题是,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿
jQuery 3.0 以下下的animate 会使用setTimeout,或者加载补丁jquery-requestAnimationFrame
降低复杂性或使用 Web Worker
Web Worker
JavaScript 运行时间过长,就会阻塞这些其他工作,可能导致丢帧。可以把不需要DOM访问权限,纯运算工作放到Web Worker。数据操作或遍历(例如排序或搜索)往往很适合这种模型,加载和模型生成也是如此
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = evt.data;
});
微任务
对应需要DOM 访问权限的。就必须放在主进程上执行,可以使用批量处理办法,将大型任务分割为微任务,每个微任务所占时间不超过几毫秒,并且在每帧的 requestAnimationFrame 处理程序内运行。
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var taskFinishTime;
do {
var nextTask = taskList.pop();
processTask(nextTask);
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 3);
if (taskList.length > 0)
requestAnimationFrame(processTaskList);
}
此方法会产生 UX 和 UI 后果,您将需要使用进度或活动指示器来确保用户知道任务正在被处理。在任何情况下,此方法都不会占用应用的主线程,从而有助于主线程始终对用户交互作出快速响应。
了解 JavaScript 的“frame tax”
在评估一个框架、库或您自己的代码时,务必逐帧评估运行 JavaScript 代码的开销。当执行性能关键的动画工作(例如变换或滚动)时,这点尤其重要。
可以通过 Chrome DevTools去获得JavaScript 运行的开销信息,评估 JavaScript 对应用性能的影响,并开始找出和修正函数运行时间过长的热点。如前所述,应当设法移除长时间运行的 JavaScript,或者若不能移除,则将其移到 Web Worker 中,腾出主线程继续执行其他任务。
避免微优化(micro-optimizing ) JavaScript
节省零点几毫秒的时间优化可是没有必要的。对于开发的是游戏或计算开销很大的应用另说。
缩小样式计算的范围并降低其复杂性
通过添加和删除元素,更改属性、类或通过动画来更改 DOM,全都会导致浏览器重新计算元素样式,在很多情况下还会对页面或页面的一部分进行布局(即自动重排)。这就是所谓的计算样式的计算。
计算样式的第一部分是创建一组匹配选择器,这实质上是浏览器计算出给指定元素应用哪些类、伪选择器和 ID。
第二部分涉及从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。在 Blink(Chrome 和 Opera 的渲染引擎)中,这些过程的开销至少在目前是大致相同的:
用于计算某元素计算样式的时间中大约有 50% 用来匹配选择器,而另一半时间用于从匹配的规则中构建 RenderStyle(计算样式的表示)
降低选择器的复杂性
复杂的选择器可能比简单地将选择器与元素匹配的开销要大得多
.box:nth-last-child(-n+1) .title {
}
.final-box-title {
}
前者在浏览器实际上必须询问“这是否为有 title 类的元素,其父元素恰好是负第 N 个子元素加上 1 个带 box 类的元素?”计算此结果可能需要大量时间。
思考:这里应该说的时css选择器写法的问题,在可以简单匹配的情况下不要写复杂匹配,条件匹配。为什么我会这么说呢?因为如果我时内容时动态的话,我自己去确定这个元素后给它加上这个类目会比浏览器自己匹配来的快?这一个我表示怀疑。
减少要计算样式的元素数量
对于许多样式更新而言是更重要的因素是减少在元素更改时需要计算的工作量。 规则简单的小树比规则复杂的大树会得到更高效地处理。
这小节,几乎只剩下一个概念。现代浏览器已经很智能,能容忍各种瞎操作操作。但我们还是要尽可能减少无效元素的数量。
测量样式重新计算的开销
测量样式重新计算可以通过 Chrome DevTools 的 Timeline 模式去记录。如果出现较大的紫色块,如上例所示,请点击记录了解到更多细节。使用此信息来开始尝试在代码中查找修正点。
使用块、元素、修饰符
BEM(块、元素、修饰符)之类的编码方法实际上纳入了上述选择器匹配的性能优势,因为它建议所有元素都有单个类,并且在需要层次结构时也纳入了类的名称
避免大型、复杂的布局和布局抖动
布局是浏览器计算各元素几何信息的过程: 元素的大小以及在页面中的位置。 根据所用的 CSS、元素的内容或父级元素,每个元素都将有显式或隐含的大小信息。此过程在 Chrome、Opera、Safari 和 Internet Explorer 中称为布局 (Layout)。 在 Firefox 中称为自动重排 (Reflow),但实际上其过程是一样的。
与样式计算相似,布局开销的直接考虑因素如下:
- 需要布局的元素数量。
- 这些布局的复杂性。
尽可能避免布局(回流)操作
更改样式时,浏览器会检查任何更改是否需要计算布局,以及是否需要更新渲染树。对“几何属性”(如宽度、高度、左侧或顶部)的更改都需要布局计算。
.box {
width: 20px;
height: 20px;
}
.box--expanded {
width: 200px;
height: 350px;
}
布局几乎总是作用到整个文档。 如果有大量元素,将需要很长时间来算出所有元素的位置和尺寸。
如果无法避免布局,关键还是要使用 Chrome DevTools 来查看布局要花多长时间,并确定布局是否为造成瓶颈的原因。工具依然是 DevTools 的 Timeline。
使用 flexbox 而不是较早的布局模型
对于相同数量的元素和相同的视觉外观Flexbox 有更好的渲染性能,1300 个框 用浮动布局和Flexbox的渲染时间分别为:14ms和3.5ms。
但是在可能的情况下,至少应研究布局模型对网站性能的影响,并且采用最大程度减少网页执行开销的模型。在任何情况下,不管是否选择 Flexbox,都应当在应用的高压力点期间尝试完全避免触发布局!
避免强制同步布局
将一帧送到屏幕会采用如下顺序:
首先 JavaScript 运行,然后计算样式,然后布局。但是,可以使用 JavaScript 强制浏览器提前执行布局。这被称为强制同步布局。
在 JavaScript 运行时,来自上一帧的所有旧布局值是已知的,并且可供您查询。在帧的开头输出一个元素(让我们称其为“框”)的高度,可以使用如下代码:
requestAnimationFrame(logBoxHeight);
function logBoxHeight() {
console.log(box.offsetHeight);
}
如果在请求此框的高度之前,已更改其样式,就会出现问题:
function logBoxHeight() {
box.classList.add('super-big');
console.log(box.offsetHeight);
}
现在,为了回答高度问题,浏览器必须先应用样式更改(由于增加了 super-big 类),然后运行布局(这里就是强制同步布局)。这时它才能返回正确的高度。这是不必要的,并且可能是开销很大的工作。
因此,始终应先批量读取样式并执行(浏览器可以使用上一帧的布局值),然后执行任何写操作:
正确完成时,以上函数应为:
function logBoxHeight() {
console.log(box.offsetHeight);
box.classList.add('super-big');
}
大部分情况下,并不需要应用样式然后查询值;使用上一帧的值就足够了。与浏览器同步(或比其提前)运行样式计算和布局可能成为瓶颈,并且您一般也不会这么做。
避免布局抖动
比强制同步布局更糟的还有,连续执行这种布局。
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),因此它必须应用样式更改,然后运行布局。每次迭代都将出现此问题!
此示例的修正方法还是先读取值,然后写入值:
var width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
简化绘制的复杂度、减小绘制区域
绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是管道中运行时间最长的任务,应尽可能避免此任务。
TL;DR
- 除 transform 或 opacity 属性之外,更改任何属性始终都会触发绘制。
- 绘制通常是像素管道中开销最大的部分;应尽可能避免绘制。
- 通过层的提升和动画的编排来减少绘制区域。
- 使用 Chrome DevTools 绘制分析器来评估绘制的复杂性和开销;应尽可能降低复杂性并减少开销。
触发布局与绘制
如果您触发布局,则总是会触发绘制,因为更改任何元素的几何属性意味着其像素需要修正!
如果更改非几何属性,例如背景、文本或阴影,也可能触发绘制。在这些情况下,不需要布局,并且管道将如下所示:
使用 Chrome DevTools 快速确定绘制瓶颈
Chrome DevTools 中的 rendering 和 Timeline 可以用于分析和修正绘制过程。
提升移动或淡出的元素
如果您已将一个元素提升到一个新层,可使用 DevTools 确认这样做已给您带来性能优势。请勿在不分析的情况下提升元素。
绘制并非总在内存中绘制单个图像。事实上,在必要时浏览器可以绘制到多个图像或合成器层。此方法的优点是,定期重绘的或通过变形在屏幕上移动的元素,可以在不影响其他元素的情况下进行处理。
Chrome、Opera 和 Firefox 上用 will-change CSS 属性,并且通过 transform 的值将创建一个新的合成器层:
.moving-element {
will-change: transform;
}
Safari 和 Mobile Safari,需要使用(滥用)3D 变形来强制创建一个新层:
.moving-element {
transform: translateZ(0);
}
减少绘制区域
减少绘制区域往往是编排您的动画和变换,使其不过多重叠,或设法避免对页面的某些部分设置动画。
降低绘制的复杂性
在谈到绘制时,一些绘制比其他绘制的开销更大。例如,绘制任何涉及模糊(例如阴影)的元素所花的时间将比(例如)绘制一个红框的时间要长。但是,对于 CSS 而言,这点并不总是很明显: background: red; 和 box-shadow: 0, 4px, 4px, rgba(0,0,0,0.5); 看起来不一定有截然不同的性能特性,但确实很不相同。
利用上述绘制分析器,您可以确定是否需要寻求其他方式来实现效果。问问自己,是否可能使用一组开销更小的样式或替代方式来实现最终结果。
您要尽可能的避免绘制的发生,特别是在动画效果中。因为每帧 10 毫秒的时间预算一般来说是不足以完成绘制工作的,尤其是在移动设备上
坚持使用仅合成的属性和管理层数
合成是将页面的已绘制部分放在一起以在屏幕上显示的过程。
此方面有两个关键因素影响页面的性能: 需要管理的合成器层数量,以及您用于动画的属性
TL;DR
- 坚持使用 transform 和 opacity 属性更改来实现动画。
- 使用 will-change 或 translateZ 提升移动的元素。
- 避免过度使用提升规则;各层都需要内存和管理开销。
使用 transform 和 opacity 属性更改来实现动画
性能最佳的像素管道版本会避免布局和绘制,只需要合成更改:
为了实现此目标,需要坚持更改可以由合成器单独处理的属性。目前只有两个属性符合条件: transforms 和 opacity:
使用 transform 和 opacity 时要注意的是,您更改这些属性所在的元素应处于其自身的合成器层。要做一个层,您必须提升元素,看这里提升您打算设置动画的元素。
管理层并避免层数激增
层往往有助于性能,知道这一点可能会诱使开发者通过以下代码来提升页面上的所有元素:
* {
will-change: transform;
transform: translateZ(0);
}
事实上,在内存有限的设备上,对性能的影响可能远远超过创建层带来的任何好处。每一层的纹理都需要上传到 GPU,使 CPU 与 GPU 之间的带宽、GPU 上可用于纹理处理的内存都受到进一步限制。
Warning: 如无必要,请勿提升元素。
使用 Chrome DevTools 来了解应用中的层
Chrome DevTools 的 Timeline 的layer。
如果您在滚动或变换之类的性能关键型操作期间花了很多时间在合成上(应当力争在 4-5 毫秒左右),则可以使用此处的信息来查看有多少层、创建层的原因,并从此处管理应用中的层数。
使输入处理程序去除抖动
输入处理程序可能是应用出现性能问题的原因,因为它们可能阻止帧完成,并且可能导致额外(且不必要)的布局工作。
TL;DR
- 避免长时间运行输入处理程序;它们可能阻止滚动。
- 不要在输入处理程序中进行样式更改。
- 使处理程序去除抖动;存储事件值并在下一个 requestAnimationFrame 回调中处理样式更改
避免长时间运行输入处理程序
当用户与页面交互时,页面的合成器线程可以获取用户的触摸输入并直接使内容移动。这不需要主线程执行任务,主线程执行的是 JavaScript、布局、样式或绘制
但是,如果您附加一个输入处理程序,例如 touchstart、touchmove 或 touchend,则合成器线程必须等待此处理程序执行完成,因为您可能选择调用 preventDefault() 并且会阻止触摸滚动发生。即使没有调用 preventDefault(),合成器也必须等待,这样用户滚动会被阻止,这就可能导致卡顿和漏掉帧。
总之,要确保您运行的任何输入处理程序应快速执行,并且允许合成器执行其工作。
避免在输入处理程序中更改样式
与滚动和触摸的处理程序相似,输入处理程序被安排在紧接任何 requestAnimationFrame 回调之前运行。
如果在这些处理程序之一内进行视觉更改,则在 requestAnimationFrame 开始时,将有样式更改等待处理。如果按照“避免大型、复杂的布局和布局抖动”的建议,在 requestAnimationFrame 回调开始时就读取视觉属性,您将触发强制同步布局!
使滚动处理程序去除抖动
上面两个问题的解决方法相同: 始终应使下一个 requestAnimationFrame 回调的视觉更改去除抖动:
function onScroll (evt) {
lastScrollY = window.scrollY;
if (scheduledAnimationFrame)
return;
scheduledAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener('scroll', onScroll);
这样做还有一个好处是使输入处理程序轻量化,效果非常好,因为现在您不用去阻止计算开销很大的代码的操作,例如滚动或触摸!
Layout和Relow 定义
MDN
MDN原文渲染页面:浏览器的工作原理,其中的描述到:
Layout 布局
he first time the size and position of nodes are determined is called layout
第一次确定节点的大小和位置称为布局
reflows 回流
Subsequent recalculations of node size and locations are called reflows
随后对节点大小和位置的重新计算称为回流
In our example, suppose the initial layout occurs before the image is returned. Since we didn’t declare the size of our image, there will be a reflow once the image size is known
假设初始布局发生在返回图像之前。由于我们没有声明图像的大小,因此一旦知道图像大小,就会有回流。
原文
Rendering Performance