HTML、CSS 和 JavaScript,是如何变成页面的?
HTML 的内容是由标记和文本组成,标记也称为标签;CSS 又称为层叠样式表,是由选择器和属性组成;JavaScript(简称为 JS),使用它可以使网页的内容“动”起来。
构建 DOM 树
浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
样式计算(Recalculate Style)
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式。
-
把 CSS 转换为浏览器能够理解的结构
CSS 样式来源主要有三种:- 通过 link 引用的外部 CSS 文件
- <style>标记内的 CSS
- 元素的 style 属性内嵌的 CSS
当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
-
转换样式表中的属性值,使其标准化
-
计算出 DOM 树中每个节点的具体样式(遵守 CSS 的继承和层叠两个规则)
- CSS 继承就是每个 DOM 节点都包含有父节点的样式。
- UserAgent 样式,它是浏览器提供的一组默认样式,如果你不提供任何样式,默认使用的就是 UserAgent 样式。
- 层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
布局阶段
计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
-
创建布局树
在显示之前,我们还要额外地构建一棵只包含可见元素的布局树。 -
布局计算
计算布局树节点的坐标位置,使其都保存在布局树中。
分层
页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
-
拥有层叠上下文属性的元素会被提升为单独的一层。明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。
-
需要剪裁(clip)的地方也会被创建为图层,如滚动条或溢出隐藏的元素。
图层绘制
渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执
行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。
栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。
总结
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
- 将 DOM 树 和 CSSOM 树合成布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程利用栅格化生成位图,此时会用 GPU 进程来进行加速。
- 提交给 浏览器主进程进行页面展示。
相关概念
当 render tree 中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。
每个页面至少需要一次回流,就是在页面第一次加载的时候,这时候是一定会发生回流的,因为要构建render tree。
在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
回流必将引起重绘,而重绘不一定会引起回流。
-
更新元素的几何属性(重排)
如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会重新进行样式计算、触发重新布局、解析之后的一系列子阶段,这个过程就叫重排。
重排需要更新完整的渲染流水线,所以开销也是最大的。 -
更新元素的绘制属性(重绘)
如果修改了元素的背景颜色,浏览器重新进行样式计算,但是布局阶段将不会被执行,直接进入绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。
相较于重排,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。 -
直接合成阶段
如果更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。
相对于重绘和重排,合成能大大提升绘制效率。
在优化 Web 性能的方法中,减少重绘、重排是一种很好的优化方式。
JavaScript 在浏览器中被解析和执行时具有阻塞的特性,也就是说,当JavaScript 代码执行时,页面的解析、渲染以及其他资源的下载都要停下来等待脚本执行完毕。
HTML <script> defer 属性:
- 如果 async="async":脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
- 如果不使用 async 且 defer="defer":脚本将在页面完成解析时执行
- 如果既不使用 async 也不使用 defer:在浏览器继续解析页面之前,立即读取并执行脚本