浏览器渲染原理:从 HTML 到像素的全链路拆解

浏览器渲染原理

什么是渲染?

渲染(render),简单来说就是浏览器将 HTML、CSS、JavaScript 等代码,转化为用户能直观看到的可视化网页界面的全过程。

更细一点讲,就是根据 HTML、CSS、JavaScript 等代码,计算浏览器视口区域每个像素点的显示颜色(浏览器牛皮),光想想都知道这个过程时极其复杂的。

渲染的时机

在浏览器地址栏输入网页地址回车之后,浏览器做了些什么?

这是一个很常见的面试题。我们现在就通过这道面试来分析了解浏览器渲染页面的时机。

简单来说就是做了两件事, 拿 HTML (网络线程)和 渲染页面 (渲染主线程)

这篇文章主要是写渲染相关的,网络线程我就稍微简单的一笔带过了

网络线程:解析输入的地址(DNS 域名解析)、建立 TCP 连接、发送 HTTP 请求(请求成功就能拿到 HTML 了)

当网络线程收到 HTML 文档之后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环机制的作用下,渲染主线程会取出并执行渲染任务,开启渲染流程(我的另一篇文章细讲了事件循环相关的知识)。

渲染过程

整个页面渲染的流程分为多个阶段,分别是:

HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画

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

下面我们来逐一了解这些渲染的阶段。

解析 HTML

首先的第一个阶段就是解析HTML,生成 DOM 和 CSSOM,我们拿到的HTML文档其实就是一个字符串,为了后续的操作和计算需要通过 HTML 和 CSS 代码生成对象结构的树。

生成 DOM 树

这个阶段首先会根据 HTML 文档,生成 DOM (Document Object Model) 树,DOM 树 是文档对象模型(Document Object Model) 的树状表现形式,它是浏览器解析 HTML 文档后,生成的一套跨平台、语言无关的编程接口,本质是把 HTML 里的所有内容(元素、属性、文本等)映射成一个个节点,再按 HTML 的嵌套关系组织成树状的对象结构(对象结构相比字符串更便于操作)。

比如我们这里有一段 HTML 代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>DOM & CSSOM 示例</title>
    <style>
        /* 标签选择器 */
        body {
            background-color: #f5f5f5;
            font-size: 16px;
        }
        /* 类选择器 */
        .content-box {
            color: #333;
            padding: 20px;
        }
        /* ID 选择器 */
        #title {
            font-weight: bold;
            color: #0066cc;
        }
    </style>
</head>
<body>
    <div class="content-box">
        <h1 id="title">DOM & CSSOM 演示</h1>
        <p>这是一段测试文本</p>
    </div>
</body>
</html>

对应的 DOM 树结构就是下面这样的(懒得画图了,将就一下)

复制代码
html(根节点,属性:lang="zh-CN")
├── head(子节点)
│   ├── meta(子节点,属性:charset="UTF-8")
│   ├── title(子节点)
│   │   └── 文本节点:"DOM & CSSOM 示例"
│   └── style(子节点,内容为 CSS 代码,仅作为文本存储,不解析)
└── body(子节点)
    └── div(子节点,属性:class="content-box")
        ├── h1(子节点,属性:id="title")
        │   └── 文本节点:"DOM & CSSOM 演示"
        └── p(子节点)
            └── 文本节点:"这是一段测试文本"
遇到 CSS 代码

主线程当解析 HTML 的过程中解析到 <link rel="stylesheet"> (外部资源引用),会触发 CSS 解析器(预解析线程),去加载对应的 CSS 代码,然后交给渲染主线程生成 CSSOM(CSS Object Model)树。

渲染主线程解析 HTML 时,遇到 <style> 标签,会先把 <style> 标签本身解析成 DOM 节点;然后读取标签内的 CSS 文本并解析生成 CSSOM(CSS Object Model)树。

每个样式表都会生成对应的 CSSOM,并放在 StyleSheetList 对象下面。

上面 HTML 解析出来的 CSSOM 结构(一个样式表的)如下(依旧将就):

复制代码
CSSOM 根(隐含,包含所有样式规则)
├── 选择器:body
│   ├── background-color: #f5f5f5
│   └── font-size: 16px
├── 选择器:.content-box
│   ├── color: #333
│   └── padding: 20px
├── 选择器:#title
│   ├── font-weight: bold
│   └── color: #0066cc (优先级高于 .content-box 的 color,最终生效)
遇到 JS 代码

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

HTML 的解析会生成 DOM 和 CSSOM,再根据它们去渲染页面,JS 代码执行则有可能会修改当前的 DOM 和 CSSOM,所以HTML的解析需要暂停,这就是 JS 阻塞 HTML 解析的根本原因。

样式计算

这一步会遍历 HTML 解析生成的 DOM 树的每一个节点进行样式计算,生成一个包含最终样式(Computed Style)的 DOM 树。

我们可以在开发者工具中的 Computed 里面查看选中元素的 Computed Style,里面包含了元素所有样式(不止是我们设置的)。

样式计算的过程也是很复杂的,后续看看再写一篇文章讲一下吧。

布局

这一步依次遍历包含最终样式(Computed Style)的 DOM 树,计算每一个节点元素的 尺寸 和 位置(相对于包含块) ,生成一个 Layout 树(C++对象)。

很多情况下DOM 树 和 Layout 树的节点不是一一对应的:

  • 一些隐藏的节点(display: none)没有几何信息(尺寸和位置),Layout 树就不会有这个节点。
  • 如果有伪元素, Layout 树则会有多的元素节点。
  • 由于有 内容必须在行盒中 的规则,Layout 树中块盒文本外会有一层匿名行盒。
  • ......

JS 可以通过布局属性拿到一些布局树的属性。

API / 属性 作用
element.getBoundingClientRect() 返回元素的边界矩形(left/top/right/bottom/width/height)
element.offsetWidth/offsetHeight 返回元素的布局宽度 / 高度
element.offsetLeft/offsetTop 返回元素相对于 offsetParent 的偏移量
element.clientWidth/element.clientHeight 返回元素的内容区宽度 / 高度

分层

这一步浏览器主线程会对布局树进行分层,如果某一个层改变后,仅会对该层进行后续处理,不影响其他层。

滚动条、堆叠上下文、transform、opacity 等样式都会影响分层结果,也是以使用 will-change属性更大程度的影响分层结果。

我们可以再开发者工具的 Layers 中查看页面的分层信息

绘制

这一步浏览器主线程会为每一个层生成绘制指令集,用于描述这一层的内容如何绘制。

主线程的渲染工作到这一步就结束了,后续的步骤会交给其他线程完成。

分块

完成绘制步骤后,主线程会将每一个图层的绘制信息提交给合成线程,剩余工作将交由合成线程完成。

合成线程首先会将每个图层分块,将其划分为更多的小区域。

合成线程会从线程池拿取多个线程来完成分块工作。

光栅化

分块完成之后就会进入光栅化阶段,合成线程会将分块信息交给 GPU 进程完成光栅化。

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

光栅化的结果就是每个块的位图(像素点颜色信息)

画

这个阶段合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。因此, transform 变形的操作是合成线程处理,与渲染主线程无关,这就是 transform 效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

Reflow(回流 / 重排)

当我们修改 DOM 元素的几何属性(位置、尺寸等)时,我们实际修改的是 CSSOM ,如果 DOM 或者 CSSOM变化了,就会重新进行样式计算生成 Layout 布局树,再将元素绘制到页面上,这个过程就是 Reflow。

它是浏览器渲染流水线中开销较高的步骤,频繁触发会显著降低页面性能,甚至造成卡顿。

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。

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

所以我们获取几何信息属性时,浏览器会立即 Reflow。

Repaint(重绘)

Repaint(重绘) 是浏览器渲染流水线中紧随回流(Reflow)的核心步骤,指当 DOM 元素的视觉样式属性 发生变化(不影响几何位置和尺寸)时,浏览器仅重新为元素绘制像素内容(如颜色、背景、阴影),无需重新计算布局的过程。

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

当改动了可见样式后,就需要重新计算,会引发 repaint。

由于元素的布局信息也属于可见样式,所以 Reflow 一定会引起 Repaint。

相关推荐
风花一世月1 分钟前
🏫 我把一整座校园的数据大屏塞进了一个 HTML 文件(零构建、双击即开,在线直接玩)
前端
变与不变8069 分钟前
GET请求知识详解
前端·javascript
智塑未来10 分钟前
网站建设哪家服务好?把交付前、交付中、交付后拆开看
大数据·前端
百度一下吧10 分钟前
H5 应用开发:Android、iOS 与鸿蒙安全区域适配实战
前端
风骏时光牛马2 小时前
数据可视化管理平台
前端
仿生狮子7 小时前
OpenAI 攻下千禧年难题之后,开发者似乎无动于衷
前端·数学·vibecoding
Lvan的前端笔记10 小时前
docker:每个前端项目一个 Nginx 容器还是只有一个Nginx容器
前端·nginx·docker
三天不学习11 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs1940512 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js