关键渲染路径

关键渲染路径

从收到HTML、CSS和JavaScript字节到对其进行必需的处理,从而将它们转变成渲染的像素。这一过程中有一些中间步骤,即关键渲染路径。

  1. 处理HTML标记并构建DOM树;
  2. 处理CSS标记并构建CSSOM树;
  3. 将DOM与CSSOM合并成一个渲染树;
  4. 根据渲染树来布局,以计算每个节点的几何信息;
  5. 将各个节点绘制到屏幕上。

阻塞渲染的CSS

关键渲染路径要求我们同时具有DOM和CSSOM才能构建渲染树,因此HTML和CSS都是阻塞渲染的资源。
我们可以通过媒体类型和媒体查询将一些CSS资源标记为不阻塞渲染。
浏览器会下载所有的CSS资源,无论是否阻塞渲染,只是不阻塞渲染的资源优先级比较低。
阻塞渲染指的是浏览器是否要暂停网页的首次渲染,直至该资源准备就绪。

JavaScript

如果将脚本移到span元素之上,就会发现,document.getElmentByTagName('span')返回null。这透露一个重要事实:脚本在文档的何处插入,就在何处执行。当HTML解析器遇到个script标记时,它会暂停构建DOM,将控制权移交给JS引擎;等JS引擎运行完毕,浏览器会从中断的地方恢复DOM构建。

  1. 当浏览器遇到一个script标记时,DOM构建将暂停,直至脚本完成执行。向script标记添加异步关键字可以指示浏览器在等待脚本可用期间不阻止DOM构建。(言下之意,在JavaScript可用,即执行时仍旧会阻塞DOM的构建和渲染)
  • async:允许脚本在后台下载,因此不阻塞。但是下载完之后,脚本开始执行,仍会阻塞渲染。在执行完之后渲染恢复。
  • defer:defer严格要求脚本的执行顺序必须与在HTML中的标记顺序一样,且等到DOM解析完毕之后再执行。因此defer脚本在下载完毕之后并不执行,而是等待前面的脚本下载执行完毕后再执行。
  1. 如果浏览器尚未完成CSSOM的下载和构建,我们却想在此时执行脚本,那么浏览器将延迟脚本的执行和DOM的构建,直至完成CSSOM的下载和构建。
    总结来说,如果遇到冲突,则优先级CSSOM的下载和构建>JS的执行>DOM的下载和构建。JS下载无异步标记时和执行同等地位,如有async标记,则下载可异步,执行优先级同上。如果有refer标记,则JS的执行都低于DOM的解析,即等待DOM解析完成后JS再执行。
1
2
window.onload // 网页所需的所有资源都已经下载并经过处理(图像不会阻止渲染,但会阻止onload事件)
document.addEventListener(‘DOMContentLoaded’, function () {}) // DOM树构建完成,但链接的资源,比如图片、样式表等可能没有加载。

关于将JS放在body的最底端

有一个疑问。关键渲染路径要求我们同时具有DOM树和CSSOM树才能够构建渲染树。那么为什么把JS放在body底部会不影响首屏渲染呢?不是一样会暂停DOM树的构建,从而无法构建渲染树吗?
使用Chrome实际测试发现,并不是将整个页面的DOM树构建完成后才能够渲染。比如,JS阻塞了DOM构建,那么实际上被阻塞之前的内容都可以渲染出来。

重绘(repaint)和重排(reflow)

无论何时总有一个初始化的页面布局伴随着一次绘制。之后,每一次改变用于构建渲染树的信息都导致以下至少一个行为:

  • 部分或整个渲染树需要重新分析并且节点尺寸需要重新计算,这被称为重排。注意,这里至少会有一次重排,即初始化页面布局。
  • 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色、visibility、outline等,屏幕上的部分内容需要更新。这样的更新被称为重绘。

什么情况会触发重绘和重排

  • 添加,删除,更新DOM节点;
  • 通过display: none隐藏一个DOM节点,触发重绘和重排;
  • 通过visibility: hidden隐藏一个DOM节点,只触发重绘,因为没有几何变换;
  • 移动或给页面中的DOM节点添加动画;
  • 添加一个样式表,调整样式属性;
  • 用户行为,如调整窗口大小,改变字号,或滚动。

一个元素的重排会引起以下相关元素的重排

  1. 子元素(例如,该元素宽高发生改变,内部子元素的布局可能也会发生改变,如子元素浮动);
  2. DOM中该元素后面的祖先级元素(例如,该元素的宽高改变,后面的浮动元素可能会发生位移);
  3. 该元素的父元素甚至祖先元素(例如,该元素的宽高发生改变,其父元素的宽高也有可能发生改变)。

善于应对的浏览器

既然渲染树变化伴随的重绘和重排代价如此巨大,浏览器一直致力于减少这些消极的影响。一个策略是干脆不做,或者至少现在不做。浏览器会基于你的脚本要求创建一个队列去储存你的代码要求的变化,然后分批去展现。使用这种方式,一些每次都要求一次重排的变化会被整合起来,只导致一次重排。浏览器可以在队列中添加变更,在经过一定的时间或者存在一定数量的变更后再执行。
但有时,你的代码会阻止浏览器优化重排,导致其刷新队列且表现出所有批次的变化。当你要求样式信息的时候会发生,比如:

  1. offsetTop/Left/Width/Height
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. getComputedStyle(),或者IE中的currentStyle。

以上这些本质上都是在请求一个节点的样式信息,任何时候你做这个,浏览器都不得不给你最新的值。为了做这个,它需要应用所有先前的更改,刷新队列,认同做重排。

最小化重绘和重排

为了减少重绘重排对用户体验的负面影响,应该减少重绘和重排,以及对样式信息的请求。

  • 不要一个个单独更改样式。对静态页面来说,明智且兼具可维护性的做法是改变类名而不是样式。对于动态改变样式来说,相较于每次微小改变都直接触及元素,更好的方法是统一在cssText变量中编辑;
1
2
3
4
5
6
7
8
9
10
11
12
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";

// better
el.className += " theclassname";

// 当top和left的值是动态计算而成时
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • “离线”批量改变和表现DOM。离线意味着不在当前DOM树做修改,你可以:

  • 通过documentFragment来保留临时变动;

  • 复制即将更新的节点,在副本上操作,然后用新节点替换旧节点;
  • 通过display: none隐藏节点(只有一次重排重绘),添加足够多的变更之后,通过display属性显示(另一次重排重绘),这样,即使大量变更也只有两次重排;

  • 不要频繁计算样式,如果你有一个样式需要计算,只取一次,将它缓存在一个变量中,并在这个变量上工作。

  • 通常,考虑一下渲染树,以及在变更后重新生效需要多少代价。比如,使用绝对定位使得元素在渲染树中成为body的子节点,所以当你给它动画效果之类的时,它不会影响太多其他节点。当你把这个元素放在这个区域其他节点的上方时,它们可能需要重绘,但不需要重排。

CSS动画优化

图层

浏览器在渲染一个页面时,会将页面分为很多个图层。图层有大有小,每个图层上有一个或多个节点。在渲染DOM时,浏览器做的工作实际是:

  1. 获取DOM后分割为多个图层;
  2. 对每个图层的节点计算样式结果;
  3. 为每个节点生成图形和位置(即重排);
  4. 将每个节点绘制填充到图层位图中(即重绘);
  5. 图层作为纹理上传至GPU;
  6. 复合多个图层到页面上生成最终屏幕图像。

Chrome中满足以下任意条件就会创建图层:

  • 3D或透视变换CSS属性,如translate3d(0, 0, 0)或translateZ(0);
  • video、iframe、canvas、webgl等元素;
  • 混合插件,如flash;
  • 对自己的opacity或transform做CSS动画;
  • 拥有CSS过滤器的元素;
  • 使用will-change属性的元素;
  • position: fixed;
  • 元素有一个兄弟元素z-index较低且包含复合层。

需要注意的是,如果图层中某个元素需要重绘,那么整个图层都需要重绘。比如一个图层包含很多节点,其中有一个gif图,gif图的每一帧都会重绘图层的其他节点,然后生成最终的图层位图。这就需要用特殊的方式强制gif单独属于一个图层(使用translateZ(0)或translate3d(0, 0, 0)),CSS动画也是一样,好在大多数浏览器自动会为CSS动画的节点创建图层。

优化技巧

  1. 避免隐式合成,如元素有一个兄弟节点z-index较低且包含复合层,元素也会成为一层。每个复合层都需要消耗额外的内存,消耗过多内存可能会导致浏览器崩溃;
  2. 动画中只使用transform和opacity,这样变换的属性不影响文档流,不依赖文档流,也不会造成重绘,一次与主线程交互后,GPU将独自执行动画,不需要主线程CPU的参与。
  3. 用CSS动画而不是JS动画。仅使用transform和opacity的CSS动画完全是工作在GPU上的。而如果使用JS动画,浏览器主线程必须计算每一帧的状态,把它们发送给GPU。除了计算和发送数据比CSS动画要慢,主线程负载也会影响动画。当主线程计算任务过多时,会造成延迟,卡顿。

参考文献