Skip to content

浏览器渲染原理 ​

✨文章摘要(AI生成)

本文深入探讨了浏览器的渲染原理和完整流程。从 HTML 解析开始,依次介绍了样式计算、布局、分层、绘制、分块渲染、光栅化到最终的合成等八个关键步骤。文章详细解释了每个阶段的工作原理,包括 DOM 树和 CSSOM 树的构建、布局树的生成、图层的划分以及 GPU 加速等核心概念。同时还讨论了重排(reflow)、重绘(repaint)和合成等性能优化相关的重要概念,为前端开发者提供了全面的浏览器渲染机制理解。

介绍 ​

将网络拿到的HTML字符串转换成页面上的像素点,这个过程就是渲染。

浏览器是如何渲染页面的 ​

当浏览器的网络线程接收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。

在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。

79750486f8a742c980c7417bc35036a9

整个渲染流程分为多个阶段,分别是:HTML解析、样式计算、布局、分层、绘制、分块渲染、光栅化、合成

每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

这样,整个渲染流程就形成了一套组织严密的生产流水线。

zeHp5.png

1、HTML解析(Parse HTML) ​

1fdeb2352c874da6a3b27c661eeaec86

解析是浏览器将通过网络接收的数据转换为 DOM 和 CSSOM 的步骤,通过渲染器在屏幕上将它们绘制成页面。

构建 DOM 树 ​

处理 HTML 标记并构造 DOM 树。

HTML 解析涉及到符号化和树的构造。HTML 标记包括开始和结束标记,以及属性名和值。如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建文档树。

DOM 树描述了文档的内容。<html>元素是第一个标签也是文档树的根节点。树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。DOM 节点的数量越多,构建 DOM 树所需的时间就越长。

dom

解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS 。为了提高解析效率,浏览器在开始解析前,会启动一个预解析线程,率先下载HTML中的外部 CSS 文件和外部的 JS 文件。

下载和解析 CSS ​

如果主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML 。这是因为下载和解析 CSS 的工作是在预解析线程中进行的,这就是 CSS 不会阻塞 HTML 解析的根本原因。

z8euA.png

查看所有样式表:document.stylesheets

给样式表添加一个规则:

js
// 随便找一个样式表,这里例如给页面中的所有div添加一个border
document.styleSheets[0].addRule('div','border:2px solid #f40 !important');

下载和解析 JS ​

如果主线程解析到 script 位置,会停止解析 HTML ,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML 。

这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。

这就是 JS 会阻塞 HTML 解析的根本原因。

同时,预解析线程可以分担一点下载JS的任务

zNlR3.png

如果我们想加快首屏的渲染,建议将script标签放在body标签底部。

当然现代浏览器都提供了非阻塞的下载方式,async和defer。

第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。

2、样式计算(Recalculate Style) ​

主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它的最终样式,称之为 Computed Style。

属性值的计算过程通常可以分为以下四个步骤:

  1. **确定声明值:**这是指在CSS样式表中,针对某个元素或选择器明确声明的属性值。例如,如果你在样式表中设置了color: blue;,那么"blue"就是这个属性的声明值。
  2. **层叠冲突:**在网页中,可能存在多个CSS规则同时应用于同一个元素,这时就会产生层叠冲突。解决冲突的过程考虑了以下三个因素:
    • 重要性(Importance):通过!important声明的规则具有最高的优先级。
    • 特殊性(Specificity):根据选择器的特殊性来确定规则的优先级,通常是通过选择器中ID、类、标签等的数量和类型来计算。
    • 源次性(Source Order):后定义的规则会覆盖先定义的规则。
  3. **使用继承:**某些属性值可以从父元素继承到子元素中。例如,如果父元素设置了font-family: Arial;,而子元素没有指定字体,则子元素会继承父元素的字体属性。
  4. **使用默认值:**如果前面的步骤都没有为属性指定值,则会应用默认值。例如,如果没有为文本颜色指定值,则默认为黑色。

在这个过程,很多预设值会变成绝对值,比如red会变成 rgb(255,0,0),相对单位会变成绝对单位,比如 em会变成 px。这一步完成之后,将会得到一棵带有样式的DOM树。

![623d54cf8343438b9c027184d6928655 (1)](http://assest.sablogs.cn/img/typora/623d54cf8343438b9c027184d6928655 (1).png)

通过控制台元素的已计算Tab,可以看到计算后的属性值,如下图。

image-20240331141003711

3、布局(Layout) ​

布局阶段会依次遍历 DOM 树的每一个节点,根据每个节点的样式信息算出节点的几何信息(尺寸和位置),得到布局(Layout)树。

布局树与DOM树的关系 ​

布局树和DOM树不一定一一对应,因为布局树是根据DOM树以及CSS样式信息生成的。

在生成布局树的过程中,一些节点可能会被忽略(比如display:none的节点),或者一些伪元素可能会被添加到布局树中。

因此,在渲染引擎中,布局树是用于计算渲染元素的位置和大小的重要数据结构,而DOM树则是HTML文档的结构表示。

匿名行盒和匿名块盒 ​

还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。

  • **匿名行盒:**一段文本中的一组连续文字,如果没有被包裹在行内元素(如<span>)中,浏览器就会生成匿名行盒来包含这些文字,使其形成一个行内盒子,从而保证这些内容能够正确地布局和渲染。
  • **匿名块盒:**一段文本通常会被视为行盒中的内容,但是,如果这段文本没有被包裹在任何明确的块级元素(如<div>)中,那么它在文档流中就不会形成一个完整的块级元素。在这种情况下,浏览器会为这段文本自动生成一个匿名块盒,使其具有块级元素的特性,从而可以在文档流中正确地占据一个块级区域,并参与到布局和排版中。

包含块的确定 ​

确定一个元素的包含块的过程完全依赖于这个元素的position属性:

  • static、relative、sticky
    • 包含块通常由其最近的祖先块级元素(如inline-block、block)决定。
  • absolute:
    • 如果最近的祖先元素也是绝对定位的,则会继续向上寻找,直到找到一个非 static 定位的祖先元素作为其包含块。
    • 如果没有找到非 static 定位的祖先元素,那么绝对定位的元素的包含块将是最初的包含块(initial containing block),即视窗(viewport)。
  • fixed:
    • 固定定位元素的包含块是视窗(viewport)本身。
  • absolute或fixed:包含块也可能是由满足以下条件的最近父级元素
    • 变换(transform)或 视点变换(perspective)的值不是 none。
    • 渲染优化(will-change)的值是 变换(transform) 或 视点变换(perspective)。
    • 滤镜(filter)的值不是 none
    • 渲染优化(contain)的值是 paint
    • 背景滤镜(backdrop-filter)的值不是 none

4、分层(Layer) ​

渲染主线程将会使用一套复杂的策略对整个布局树进行分层。

分层的好处在于,将来某一层改变之后,仅会对该层进行后续处理,不影响其他分层,从而提升效率。

每个浏览器都有自己分层策略。滚动条和跟堆叠上下文相关的属性都可能影响分层(z-index、opacity、transform),也可以通过will-change属性更大程度地影响分层结果。

will-change属性 ​

will-change 属性是一种 CSS 属性,用于提示浏览器某个元素将要发生什么改变,以便浏览器可以对相应的渲染做出优化。通常情况下,当你知道某个元素在不久的将来会发生变化时,你可以使用 will-change 属性来提前告知浏览器,以便优化性能。

这个属性可以接受多个值,常用的包括:

  • auto:浏览器自动决定优化方式。
  • scroll-position:告知浏览器元素的滚动位置发生变化。
  • transform:告知浏览器元素的变换(如旋转、缩放等)发生变化。
  • opacity:告知浏览器元素的透明度发生变化。
  • contents:告知浏览器元素的内容发生变化。

层边框 ​

我们使用控制台的层边框可以查看到哪些元素形成了单独的渲染层

image-20240331151523505

3D视图 ​

控制台的3D视图也能看出多层的结构,这块后面会单独进行讲解

image-20240331151629223

5、绘制(Paint) ​

渲染主线程的工作到此为止,剩余步骤交给其他线程完成。

主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。

绘制指令集通常指的是由浏览器引擎生成的一系列指令,用于描述如何在屏幕上渲染网页内容。这些指令集被发送到图形硬件或操作系统的图形 API 中,以实际绘制网页内容。

586db50506a341daaf4af59290981181

绘制指令类似于canvas的操作方法:

  • 移动画笔到 (x,y) 绘制宽为w,高为h的矩形…

实际上,canvas是浏览器将绘制过程封装后提供给开发者的工具。

6、分块渲染(Tiling) ​

分块:

  • 在布局完成后,浏览器会将页面内容划分为多个块(或称为块级渲染区域)。
  • 通常情况下,这些块是基于文档流中的块级元素进行划分的,每个块对应一个连续的垂直区域。

按需渲染:

  • 一旦页面被划分为块,浏览器可以根据用户的视口位置和滚动行为,选择性地加载和渲染这些块。
  • 通常情况下,浏览器会优先加载并渲染用户当前可见区域内的块,以提高用户感知的加载速度。

异步渲染:

  • 在分块渲染过程中,浏览器通常会采用异步渲染的方式,即在后台进行渲染操作,不会阻塞页面的主线程。
  • 这样可以确保用户可以立即与页面进行交互,而不会感受到卡顿或延迟。

完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。合成线程首先对每个图层进行分块,将其划分为更多的小区域。它会从线程池中拿取多个线程来完成分块工作。

合成线程和渲染主线程都位于渲染进程里。

zEHfo.png

  • Tiling(瓦片化): 瓦片化是一种优化技术,用于加速图形渲染和显示。它将屏幕分割成小块(瓦片),并且分别渲染每个瓦片,而不是一次性渲染整个屏幕。这有助于提高渲染的效率,特别是在处理大型、复杂的图像或页面时。瓦片化可以应用于各种场景,包括图形渲染、地图显示和浏览器页面渲染等。
  • Tiles(瓦片): 在浏览器分块渲染中,"tiles"指的是将网页内容分割成多个瓦片,然后分别渲染这些瓦片的过程。这种分块渲染技术使得浏览器可以更加高效地处理大型页面。当您滚动网页时,浏览器只需要重新渲染那些进入或离开视图的瓦片,而不是整个页面,这可以显著减少渲染的工作量,提高滚动的流畅度和响应速度。

7、光栅化(Raster) ​

光栅化或是称为栅格化。 栅格化的过程就是将图块转化成位图的过程。

z7PVL.png

图块(tiling 是栅格化的最小单位,栅格化会优先离视口最近的图块来进行渲染,离得远的会降级栅格化的优先级,同时在渲染的过程中会借用 GPU 来加速生成,最后将生成的位图保存到 GPU 的内存中。

GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

img

8、合成 ​

  1. 生成绘制指令:
    • 合成线程会根据图块的内容生成绘制指令,通常是DrawQuad。
    • DrawQuad包含了绘制图块的具体指令,包括绘制的位置、大小、内容等信息。
  2. 创建CompositorFrame对象:
    • 生成的绘制指令会被放入一个称为CompositorFrame的对象中。CompositorFrame是一个包含了所有需要在屏幕上绘制的绘制指令的容器。
  3. 提交到浏览器主线程:
    • 一旦CompositorFrame准备就绪,合成线程会将其提交给浏览器主线程。
  4. 合成器(Viz)渲染:
    • 浏览器主线程收到CompositorFrame后,将会调用合成器(通常是Viz)来执行合成操作。合成器会解析CompositorFrame中的绘制指令,并使用OpenGL等图形API来渲染这些指令。
  5. 输出到屏幕:
    • 合成器渲染完成后,将得到的像素点输出到屏幕上,最终呈现给用户。
    • 这些像素点包含了所有图块的内容,经过合成后的最终画面。

img

为什么不由合成线程直接交给硬件绘图? ​

其实是因为合成线程和渲染主线程都属于渲染进程,渲染进程处于沙盒中,无法进行系统调度,即无法直接与硬件GPU通信,所以需要GPU进程中转一下。

重排,重绘,合成 ​

整体的流程如下图

a2d8ae86cd7d448b91cc7e5fc4408312

更新了元素的几何属性(重排) ​

重排(Reflow) 的本质就是重新计算Layout布局树。

当进行了会影响布局树的操作后,需要重新计算布局树,就会引发重新布局。

浏览器为了避免连续的多次操作导致布局树反复计算,就会合并这些操作,生成一个渲染任务,等到下一次事件循环再进行计算。

所以,改动CSS属性所造成的Reflow是异步完成的。

正因为如此,当 JS 获取布局属性时(如clientWidth),就可能造成无法获取到最新的布局信息。

于是浏览器在反复权衡下,最终决定获取属性时,立即 Reflow(同步)。

518c5f88ba1049fe8ba8b267e0d9cd2d

更新元素的绘制属性(重绘) ​

重绘(repaint) 的本质就是重新根据分层信息计算了绘制指令。

当改动可见样式后,就需要重新计算绘制指令,引发 Repaint。由于元素的布局(Layout)信息也属于可见样式,所以 Reflow 一定会引起 Repaint。

rrrrrrepaint

直接合成 ​

哪些属性可以直接合成?

浏览器会把一些繁重的任务交给 GPU 处理,GPU 是为图形渲染的复杂的计算来处理的。并不是所有的 css 属性都能触发 GPU 加速,只有少数的属性可以,比如:

  • transform:translate3d()或 translateZ()
  • opacity
  • filter
  • will-change

但是也要注意使用 GPU 渲染可能会带来的问题:

  • 过多地开启硬件加速可能会耗费较多的内存,这一点在移动端浏览器上尤为明显,会导致渲染的结果很差,
  • 使用 GPU 渲染会影响字体的抗锯齿效果

参考 ​

Last updated: