Skip to content

浏览器渲染原理

✨文章摘要(AI生成)

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

介绍

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

浏览器是如何渲染页面的

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

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

79750486f8a742c980c7417bc35036a9

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

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

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

zeHp5.png

1、HTML解析(Parse HTML)

1fdeb2352c874da6a3b27c661eeaec86

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

构建 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标签底部。

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

第一步完成后,会得到 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-blockblock)决定。
  • 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: