Appearance
parse 解析 HTML(HTML ==> DOM 树 + CSSOM 树)
document DOM 树
拿到这个 HTML 字符串之后,为了方便操作,将 HTML 转换成对象(树结构),同时也提供了 js 操作 html 的能力
CSS DOM 树
css 也同理,会生成一个 css DOM 树,其中根节点是样式表
样式表:
可以通过 document.styleSheets
获取网页下的所有样式表,也可以操作样式表
HTML 解析过程
HTML 解析遇到 CSS
为了提高解析效率,浏览器会启动一个预解析器先先下载和解析 CSS (生成 css DOM 还是交给渲染主线程,只是帮忙做一些解析工作),CSS 解析是在另一个线程上执行,因 此 CSS 不会阻塞 HTML 的解析
HTML 解析遇到 CSS 总结:
- 解析过程中遇到 CSS 解析 CSS,遇到 JS 解析 JS。为了提高效率,浏览器在开始解析前 ,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和外部的 JS 文件。
- 如果主线程解析到
link
位置,此时外部的 CSS 文件还没有下载解析好,主线程不会 等待,继续解析后续的 HTML。这是因为 CSS 的下载和解析是在预解析线程中进行的。这 就是 CSS 不会阻塞 HTML 解析的根本原因
HTML 解析遇到 JS
- 渲染主线程遇到 JS 时必须暂停一切行为,等待下载执行完后才能继续,因为 JS 代 码有可能会改动 HTML 上的 DOM 元素
- 预解析线程可以分担一点下载 jS 任务
HTML 解析遇到 JS 总结:
- 当主线程解析到
script
位置时,会停止解析 HTML,等待 JS 文件下载并将全局代码 解析执行完成后,才能继续解析 HTML。 - 这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。 这就是 JS 阻塞 HTML 解析的根本原因。
HTML 解析完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部 样式、行内样式都会包含在 CSSOM 树中。
style 样式计算 (DOM 树 + CSSOM 树 ==> Computed DOM 树)
获取所有计算好的样式 getComputedStyle
总结:
- 在样式计算中,主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样 式,称之为
Computed Style
- 在这一过程中,很多预设值会变成绝对值,比如
red
会变成rgb(255,0,0)
,相对单 位会变成绝对单位,比如em
会变成px
- 这一步完成后,会得到一颗带有样式的 DOM 树。
layout 布局 (Computed DOM 树 ==> 布局树)
总结:
- 布局阶段会依次遍历 DOM 树(样式计算后得到的 DOM 树)的每一个节点,计算每个节点 的几何信息。例如节点的宽高,相对包含块的位置。
- 大部分时候,DOM 树和布局树并非一一对应
- 比如
display:none
的节点没有几何信息,因此不会生成到布局树,又比如使用了伪元 素选择器,虽然 DOM 树中不存在这些伪元素节点,但它拥有几何信息,所以会生成到布 局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
- 比如说
<head><>link>
标签等,是被隐藏的,因为浏览器默认让它隐藏。
- 获取布局树的信息,布局树并不是 DOM 树,它是其它的对象(浏览器 C++写的)
document.body.clientWidth
layer 分层 (布局树 ==> 分层)
总结:
- 主线程会使用一套复杂的策略对整个布局树进行分层。
- 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提高效率。
- 滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可 以通过
will-change
属性告诉浏览器这个节点未来将会改变,从而更大程度的影响分 层结果。
paint 绘制 (分层后 ==> 为每一层生成如何绘制的指令)
渲染主线程做到这一步就到此为止,剩余工作交给其它线程完成:
总结:
- 主线程会为每个层单独绘制指令集,用于描述这一层的内容该如何画出来。
tiling 分块 (每个图层 ==> 在合成线程中分成多个小区域)
总结:
- 完成绘制后,主线程将每个图层的绘制信息提交个合成线程,剩余工作将由合成线程完成 。
- 合成线程首先对每个图层进行分块,将其划分为更多的小区域。
- 它会从线程池中拿取多个线程来完成分块工作
raster 光栅化 (块 ==> 在 GPU 进程中将每个块变成位图)
总结:
- 分块完成后,进入光栅化阶段。
- 合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。
- GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
- 光栅化的结果,就是一块一块的位图。
draw 画 (位图 ==> GPU 最终呈现)
总结:
- 合成线程拿到每个层、每个块的位图后,生成一个个指引(quad)信息。
- 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。变 形发生在合成线程中,与渲染主线程无关,这就是 transform 效率高的本质原因。
- 合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件, 完成最终的屏幕成像。
完整过程
渲染主线程:
HTML ==> DOM 树 + CSSOM 树 ==> Computed DOM 树(样式计算后) ==> 布局树 ==> 分层 ==> 绘制(为每一层生成如何绘制的指令)
合成线程:分块(每个图层 ==> 在合成线程中分成多个小区域)
GPU: 光栅化(块 ==> 在 GPU 进程中将每个块变成位图) ==> 画(最终呈现)
常见问题
什么是 reflow(回流)?
- reflow 的本质就是改变了 CSSOM 树,进而改变了 Computed DOM 树(样式计算后生成的 树),导致需要重新计算 layout 树。
- 为了避免连续的多次操作导致布局树的反复计算,浏览器会合并这些操作,当 JS 代码全 部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。
- 为了避免 JS 获取布局信息无法及时获取最新的布局信息,浏览器会在获取属性时立即 reflow
什么是 repaint (重绘)?
- 重绘的本质改变了计算样式树,但布局树未改动,就是重新根据分层信息计算了绘制指 令,比如只改变 dom 的可见样式(颜色)
- 由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。
为什么 transform 效率高?
- 因为 transform 既不胡影响布局也不会影响绘制指令,它影响的只是渲染流程的最后 一个 draw 阶段。
- 由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之 ,渲染主线程无论如何忙碌,甚至卡死,也不会影响 transform 的变化。所以 transform 的效率高。