关键渲染路径
浏览器渲染分为以下步骤(包括但不限于)
- 解析
- 构建 HTML 树(DOM 树)
- 构建 CSS 树(CSSOM 树)
- 合并两棵树为一棵(Render Tree)
- Layout 布局(文档流/盒模型/大小/位置等计算)主动要 HTML 布局相关
- paint 绘制(颜色/阴影等)主要动 css 绘制相关属性
- compsite 合并 根据层叠关系展示页面
当页面内容发生更新时,则可能会触发回流和重绘。
开始解析
当浏览器收到服务器发来的数据时,它就开始解析收到的信息。解析是指浏览器将数据转换为 DOM 和 CSSOM 的过程,解析完成后会通过渲染器把 DOM 和 CSS 绘制成页面呈现在屏幕上。
第一步:构建 DOM 树
在解析开始时,会先处理 HTML 标记并构造 DOM 树。
构造 DOM 树实际上就是 HTML 标记语言转化为 DOM 节点的过程。HTML 标记包括开始和结束标记,以及属性名和值等。
当 HTML 的格式良好时,解析它会非常快速,与此同时,构造 DOM 树的过程也会更快一些。
DOM 树描述文档的内容。<html>元素是第一个标签也是文档树的根节点。树能够反映不同 HTML 标记之间的关系和层次结构。
嵌套在标记中的标记则是子节点。DOM 的节点数量越多,构建 DOM 树所需的时间就越长。
script 脚本
除了节点数量之外,script脚本会直接影响构建DOM树的速度。
我们在 HTML 结构中除了例如<div>这样的普通元素外,还会放以下内容:
- 图片等静态资源
CSS样式表script脚本- ...
目前的 HTML 解析器非常智能,它遇到图片这样的静态资源以及 CSS 文件时会继续解析,因为它们并不会影响 DOM 结构。我们称这些资源为非阻塞资源。
但没有async和defer属性的script是例外。
脚本代码会阻塞渲染并停止 HTML 的解析。因为脚本代码可能会修改 DOM 树的结构,所以这时就停止继续解析了。
等待和获取 CSS 并不会阻塞 HTML 的解析或者下载,但是 CSS 有可能阻塞script脚本,所以 HTML 解析器在等待 script 的同时,script 可能在等待 CSS,于是 CSS 就相当于间接把解析过程给阻塞了。
为了确保脚本不会阻塞进行,可以给 script 添加 async 属性或 defer 属性
预加载扫描器
预加载扫描器是为了加速解析的过程,尽可能快地让页面渲染出内容而诞生的程序。
浏览器构建 DOM 树时,这个过程占用了主线程。
预加载扫描器是通过扫描并分析出高优先级的资源,比如 CSS、JavaScript、字体等资源,它在后台检索资源,并且提前运行或者下载,等到主 HTML 解析器执行到请求的资源时,资源可能已经准备好了。
比如下面的代码
html1<link rel="stylesheet" src="styles.css" /> 2<script src="myscript.js"></script> 3<img src="myimage.jpg" alt="image description" /> 4<script src="anotherscript.js"></script>
当主线程在解析 HTML 和 CSS 时,预加载扫描器可能提前扫描到脚本和图像,并开始下载它们。
这样处理的结果是减少了阻塞的发生。
第二步:构建 CSSOM 树
第二步是处理 CSS 并构建 CSSOM 树。CSSOM 跟 DOM 是类似的,是浏览器将 CSS 的规则转化为可以理解的数据的一种结构关系映射。
其他过程
当构建 CSSOM 树时,其他资源,包括 JavaScript 也正在被下载(通过预加载扫描器),JavaScript 代码被解释、编译、解析和执行。脚本会被解析为抽象语法树(AST)。
开始渲染
当构建完 DOM 树和 CSSOM 树后,渲染步骤开始了,它包括样式/布局/绘制,可能还包括合成等。
第三步:构建 Render 树
渲染刚开始时,会将 DOM 树和 CSSOM 树组合成 Render 树,它的过程是从 DOM 树的根节点开始构建,将每个节点都应用上 CSSOM 的规则。
这里有个小细节:head 标签和script标签天生具有display:none,这些标签和其子节点以及其他具有display:none的标签不会出现的 Render 树上
Render 树会保存所有具有内容和计算样式的可见节点,并根据 CSS 级联确定每个节点的计算方式。
第四步:Layout 阶段
布局就是确定所有应该呈现在页面上的节点的宽度、高度和位置,以及确定节点相互之间的位置关系等的一种过程。
当构建完 Render 树后,开始布局。渲染树标识显示哪些节点(即使不可见)及其计算样式,但不标识每个节点的尺寸或位置。为了确定每个对象的确切大小和位置,浏览器从渲染树的根开始遍历它并计算每个节点的布局。
布局通常从 body 开始,用每个元素的框模型来排列body子孙元素的尺寸,为它们提供占位空间。
这个阶段有可能触发回流,回流的意思是重新对页面的某些内容或者整个文档的大小和位置进行确定。
第一次确定节点大小和位置称为布局,随后任何对节点大小和位置的重新计算称为回流。
第五步:Paint 阶段
这一阶段是将各个节点绘制到到屏幕上的阶段。
在这个阶段,浏览器就可以根据布局计算的框来转化成屏幕上的实际像素,并将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影、图像等。
第六步:Composite 合成
当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。
回流和重绘
当打开页面时,浏览器会通过渲染引擎执行上述操作,最终形成一个页面。
当使用 JavaScript 更新样式时,又分为以下三种渲染方式
回流
根据渲染规则,我们可以发现,假设我把 html 改变了(位置、盒模型、大小)等有关的 css 或本身(删除节点),这一过程页面更新时可能会经过 layout 布局阶段,这就叫回流。
回流的一个例子是一个从服务器获取到一张图片,并放置在原来没有宽高的盒子里,因为这个图片改变了当前页面的布局,我们称之为回流了,回流后又会重新经过 layout、parint、composite 阶段。所以回流必然引起重绘。
重绘
如果我们只是改变了 background-color,页面更新时就可能不会经过 layout,会直接经过 paint 绘制阶段,这就叫重绘,重绘不会引起回流。
影响回流的操作
- 添加/删除元素
- 操作 Styles
- display:none
- 移动元素位置
- 读取 offsetXXX、scrollXXX、clientXXX 等几何属性
- 修改浏览器大小、字体大小
不触发回流和重绘的情况
页面更新时,有一些属性的变化不会触发回流和重绘。
这些属性是:
- translate
- scale
- rotate
- opacity
拿 transform举例,这个属性的变化既不会经过 layout 布局阶段,又不会经过 paint 阶段,会直接到达合成阶段 compsite。(虽然有可能改变位置(translate)、大小(scale)),这种情况下,浏览器更新页面时渲染效率提高了。
这里有一篇比较详细的解释:
[译] 高性能动画(为什么使用 translate() 比 pos: abs top/left 移动元素更好)