浏览器工作原理

从地址栏输入 URL 到页面显示,浏览器依次完成 URL 校验 → DNS 解析 → 网络请求 → HTML/CSS/JS 解析 → 渲染树构建 → 布局 → 绘制 → 合成。理解这条 关键渲染路径(Critical Rendering Path),是掌握浏览器内部机制的基础。

本文按时间顺序梳理各阶段的核心机制,重点放在 DOM/CSSOM 构建与渲染管线。下图按 客户端 → 网络层 → 解析层 → 渲染层 四阶段展示完整流程:

一、客户端:URL 输入与校验

用户在地址栏输入 URL、点击链接或提交表单,都属于 导航(Navigation) 的触发动作。这一步发生在浏览器内部,尚未进入网络请求——浏览器首先要对 URL 做校验和解析,确认可以安全、合法地发起导航后,才会进入 DNS 和网络阶段。

1、URL 解析

浏览器按 URL 标准 将输入字符串解析为结构化对象,主要包含:

部分 示例 说明
scheme(协议) https 决定使用何种加载机制
host(主机) example.com 域名或 IP
port(端口) 443 可省略,使用协议默认端口
path(路径) /docs/page 资源路径
query(查询) ?id=1 查询参数
fragment(片段) #section 仅用于页面内定位,不会发送给服务器

用户可能只输入 example.com,浏览器会自动补全为 https://example.com/(具体策略因浏览器和 HSTS 配置而异)。

2、校验与安全检查

解析完成后,浏览器还会做一系列检查,任一环节失败都可能中止导航

  • 协议合法性javascript:data: 等特殊 scheme 走不同处理路径,不会发起普通 HTTP 请求
  • HSTS 升级:若域名在 HSTS 预加载列表中,强制将 http:// 升级为 https://
  • 混合内容拦截:HTTPS 页面中的不安全 HTTP 子资源会被阻止或警告
  • Safe Browsing:对照恶意网站/钓鱼名单(如 Google Safe Browsing)
  • CSP / 权限策略:部分导航可能受 Content-Security-Policy 约束

只有校验通过后,浏览器才会创建网络请求,进入 DNS 解析阶段。

二、域名解析(DNS)

域名解析就是查找域名对应 IP 地址的过程。浏览器不会每次都走完整 DNS 查询,而是按层级逐级查找缓存:

顺序 查找位置 说明
1 浏览器 DNS 缓存 命中则直接返回,有时效限制
2 操作系统 DNS 缓存 浏览器未命中时由 OS 提供
3 本地 hosts 文件 手动配置的域名映射
4 本地 DNS 服务器(递归解析) 通常由运营商或路由器分配

本地 DNS 服务器采用 迭代查询:依次向根域名服务器 → 顶级域名服务器 → 权威域名服务器询问,每一级只返回「下一级该问谁」,不会直接返回最终 IP(除非它自己已有缓存)。

解析成功后,结果会缓存到本地 DNS 服务器;TTL 过期后需重新查询。解析失败则页面无法访问。

三、网络通讯

获取 IP 后,浏览器与服务器建立连接并收发 HTTP 报文。

1、建立连接

  1. TCP 三次握手:客户端与服务器建立可靠传输通道
  2. TLS 握手(HTTPS):协商加密算法、验证证书、交换密钥(HTTP 明文传输则跳过此步)

现代浏览器在 HTTPS 场景下还会启用 TLS 1.3,通常 1-RTT 即可完成握手;支持 0-RTT 的站点可在复连时进一步减少延迟。

2、发送与接收

浏览器构造 HTTP 请求报文(方法、路径、Header、Body),经 TCP 封装后依次经过传输层 → 网络层 → 数据链路层 → 物理层到达服务器。

服务器处理请求后返回 HTTP 响应报文(状态码、Header、HTML/CSS/JS 等资源)。部分服务器会先发 103 Early Hints,在 HTML 生成完毕前提示浏览器预加载 CSS/JS,缩短等待时间。

3、连接复用与协议演进

协议 特点
HTTP/1.1 默认 Keep-Alive,同一 TCP 连接复用多个请求
HTTP/2 多路复用、Header 压缩、Server Push(已逐步弃用 Push)
HTTP/3 基于 QUIC(UDP),消除队头阻塞,移动网络更稳定

只有在连接关闭或超时后,才会通过 TCP 四次挥手断开连接。

4、资源加载优先级

浏览器会根据资源类型和位置自动分配 fetch priority

  • HTML 文档:最高
  • <head> 中的 CSS、同步 <script>:高
  • <img fetchpriority="high">、LCP 图片:可手动提升
  • async/defer 脚本、预加载资源:中低
  • lazy 图片、低优先级 prefetch:低

四、解析 HTML

HTML 解析分为 词法分析(生成 token)和 语法分析(构建 DOM 树)两个阶段。HTML 解析器不会等待整个文档下载完毕——流式解析会在收到部分字节后就开始构建 DOM,这也是首屏能逐步渲染的基础。

1、词法分析

词法分析将字符流解析成 token。HTML 语法容错性强,无法使用常规编译器式解析器,浏览器采用基于 状态机 的自定义词法分析器。

词法分析器(Tokenizer):负责将输入内容分解成一个个有效标记(token)。

<p class="a">text</p> 为例,生成的 token 序列大致为:

Token 类型 内容
标签开始 <p
属性 class="a"
标签结束 >
文本 text
结束标签 </p>

状态机从 数据状态(Data state) 出发:

  • 遇到普通字符 → 进入文本节点
  • 遇到 < → 进入 标签状态
    • 下一个字符是 ! → 可能是注释或 CDATA
    • 下一个字符是字母 → 开始标签,持续接收直到 >
    • 下一个字符是 / → 结束标签,持续接收直到 >,然后回到数据状态

本质上,状态机把每种 token 的「特征字符链」拆成独立状态,再串联成一张状态转移图。

2、语法分析(构建 DOM 树)

树构建器(Tree Builder):根据 HTML 语法规则分析 token 序列,构建 DOM 树。

语法分析的过程就是构建 DOM 树的过程。解析器在消费 token 的同时创建 Document 对象,以它为根节点不断插入子元素。规范定义了每个 token 对应的 DOM 元素;解析器还会维护一个 开放元素栈 来跟踪嵌套关系,并纠正标签嵌套错误。

栈的操作规则(Tree Builder 消费的是 完整 token,不是词法分析阶段的中间态):

  • 栈顶元素即当前正在构建的节点
  • 遇到 StartTag token(含标签名与全部属性)→ 创建元素、写入属性、挂到父节点后 入栈
  • 遇到 Character token(文本)→ 插入为栈顶元素的子节点(不入栈;相邻文本会合并)
  • 遇到 Comment token → 作为栈顶元素的子节点
  • 遇到 EndTag token出栈并校验是否与栈顶标签匹配

3、浏览器的容错机制

浏览 HTML 页面时几乎不会看到「语法无效」的错误提示——浏览器会按 HTML 解析算法 自动修正无效内容并继续解析。例如未闭合的标签、错误的嵌套顺序等,都会被容错处理。

4、遇到 script 标签

HTML 解析器遇到 <script> 时会 暂停 DOM 构建,触发 JavaScript 的下载与执行(详见下一节)。默认情况下,这会阻塞后续 HTML 的解析。

五、解析 JavaScript

HTML 解析器遇到 <script> 时,会触发 JavaScript 的 下载 → 解析 → 编译 → 执行。其中解析阶段与 HTML、CSS 类似,同样分为 词法分析语法分析;编译与执行在 主线程 完成。

下载脚本 → 词法分析(Token)→ 语法分析(AST)→ 编译(字节码)→ 执行 → 恢复 HTML 解析

1、词法分析(Tokenize)

词法分析器(Lexer / Scanner)将 JS 源码字符流切分为一个个 Token,不关心语法结构,只识别「词」的边界和类型。

const sum = a + 1; 为例:

Token 类型
Keyword const
Identifier sum
Punctuator =
Identifier a
Punctuator +
Numeric 1
Punctuator ;

常见 Token 类型包括:关键字ifreturnasync)、标识符字面量(数字、字符串、正则)、运算符分隔符

词法分析还会处理:

  • 自动分号插入(ASI) 的前置判断
  • 模板字符串正则字面量 等上下文相关 token
  • 注释 直接丢弃,不参与后续分析

2、语法分析(Parse → AST)

语法分析器(Parser)根据 ECMAScript 语法规范,将 Token 流组装成 抽象语法树(AST,Abstract Syntax Tree)。每个 AST 节点对应一种语法结构,例如变量声明、函数调用、if 语句等。

Program
└── VariableDeclaration (const sum)
└── VariableDeclarator
├── Identifier: sum
└── BinaryExpression (+)
├── Identifier: a
└── Literal: 1

若源码不符合语法规则,Parser 在此阶段抛出 SyntaxError,脚本不会进入执行阶段——这与 HTML 的容错解析不同,JS 语法错误是硬失败。

解析完成后,引擎还会做 早期错误(Early Error) 检查,例如:

  • let 重复声明
  • return 出现在非函数顶层
  • 严格模式下的非法 with 语句

3、编译与执行

现代引擎(如 V8)不会直接把 AST 交给解释器逐行运行,典型流水线为:

  1. Parser 生成 AST
  2. Ignition 将 AST 编译为 字节码(Bytecode) 并执行
  3. TurboFan 对热点代码做 JIT 优化,生成高效机器码
源码 → AST → 字节码 →(热点)优化机器码

执行期间可以读写 DOM、触发重排重绘、注册事件监听等——都在主线程上进行,因此长时间运行的 JS 会阻塞页面响应。

4、script 标签与 HTML 解析的阻塞

默认情况下,普通 <script><script src="..."> 会:

  1. 阻塞 HTML 解析——解析器暂停,等待脚本下载
  2. 阻塞 DOM 构建——脚本执行期间不能继续插入节点
  3. 可能阻塞渲染——若脚本修改 DOM 或读取布局属性

CSS 也会阻塞脚本执行:浏览器需先完成样式表解析,才能执行依赖样式的 JavaScript(避免 JS 读到错误的样式计算结果)。

5、async 与 defer

通过 asyncdefer 属性,可以改变脚本的 下载时机执行时机,从而减少对 HTML 解析的阻塞:

属性 下载 HTML 解析 执行时机 执行顺序
默认(无属性) 同步,阻塞解析 暂停 下载完立即解析并执行 按文档顺序
defer 异步,不阻塞解析 继续 DOM 解析完成后、DOMContentLoaded 按文档顺序
async 异步,不阻塞解析 继续 下载完立即解析并执行 不保证顺序
type="module" 异步(类似 defer) 继续 DOM 解析完成后 按文档顺序,支持 ES Module

注意:asyncdefer 只影响 何时下载、何时执行,不影响 JS 本身的词法/语法分析过程——一旦开始执行,解析流程相同。

<!-- 默认:下载与执行期间阻塞 HTML 解析 -->
<script src="/critical.js"></script>

<!-- defer:异步下载,DOM 解析完成后按顺序执行 -->
<script defer src="/app.js"></script>
<script defer src="/app-chunk.js"></script>

<!-- async:异步下载,下载完立即执行,不保证顺序 -->
<script async src="/analytics.js"></script>

执行时序对比(简化):

默认 script:  [HTML]──停──[下载 → 解析 → 执行 JS]──[继续 HTML]──[DOMContentLoaded]
defer: [HTML 持续解析 + JS 异步下载]──[解析 → 执行 JS]──[DOMContentLoaded]
async: [HTML 持续解析]──[JS 下载完即解析执行,时机不确定]

6、其他加载方式

  • type="module":默认 defer 语义,自动严格模式,支持 import/export
  • 动态 import():运行时按需加载,不阻塞 HTML 解析
  • document.createElement('script'):动态插入的脚本默认等效于 async

六、解析 CSS

构建 DOM 树的过程中遇到 CSS 资源会同步解析。CSS 同样经过词法分析和语法分析,最终生成 CSSOM 树(CSS Object Model)。

样式来源主要有三种:

  • <link> 引用的外部 CSS 文件
  • <style> 标签内的 CSS
  • 元素 style 属性中的内联 CSS

1、词法分析

浏览器使用 Flex 等工具将 CSS 字符流解析为 token。

2、语法分析(构建 CSSOM 树)

解析器将 token 组装成 StyleSheet 对象,每个对象包含多条 CSS 规则;每条规则包含一个选择器和一组属性声明。

StyleSheet
├── CSSRule { selector: "body", properties: { margin: 0 } }
├── CSSRule { selector: ".title", properties: { font-size: 24px } }
└── CSSRule { selector: "#header", properties: { display: flex } }

可在 DevTools Console 中通过 document.styleSheets 查看解析结果。

3、生成选择器索引(Rule Map)

为加速样式匹配,浏览器将 CSS 规则按 最右侧选择器 的类型(id、class、标签、伪类)分别存入哈希表:

CompactRuleMap m_idRules;
CompactRuleMap m_classRules;
CompactRuleMap m_tagRules;
CompactRuleMap m_shadowPseudoElementRules;

匹配时先按 id → class → 伪元素 → 标签的顺序快速取出候选规则,再逐条检查复合选择器左侧是否也匹配当前元素。

4、CSS 阻塞渲染

CSS 是 渲染阻塞资源:浏览器必须等 CSSOM 构建完成,才能生成渲染树并进行首次绘制。@import 会串行触发额外请求,进一步延迟 CSSOM 的构建。

七、构建渲染树

1、渲染树的概念

DOM 树描述文档结构,CSSOM 树描述样式规则;渲染树(Render Tree) 则是二者的交集——只包含需要显示的节点及其计算后的样式。

Firefox 称渲染树中的节点为 Frame,WebKit/Blink 称为 RenderObject。每个 RenderObject 对应一个矩形区域,其类型由 display 等 CSS 属性决定:

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
switch (style->display()) {
case NONE: break;
case INLINE: o = new RenderInline(node); break;
case BLOCK: o = new RenderBlock(node); break;
case INLINE_BLOCK: o = new RenderBlock(node); break;
case LIST_ITEM: o = new RenderListItem(node); break;
// ...
}
return o;
}

2、构建过程

从 DOM 根节点 document 开始 深度优先遍历,对每个可见节点:

  1. 在 Rule Map 中匹配适用的 CSS 规则
  2. 计算最终样式(含继承、层叠、默认值)
  3. 创建对应的 RenderObject 加入渲染树

2.1 计算样式

1)选择器匹配

对每个节点,按 id → class → 伪元素 → 标签 → 通配符的顺序取出候选规则。若复合选择器最右侧匹配,再递归检查左侧选择器是否也匹配。

2)设置 style

找到匹配规则后,计算各 CSS 属性的最终值:

  • 继承属性:沿 DOM 树向上查找祖先元素的值,找不到则用初始值
  • 层叠合并:多个规则命中同一节点时,按优先级(!important > 内联 > ID > class > 标签 > 通配符)合并
  • 样式结构共享:浏览器将常用样式组合缓存为共享对象,减少内存占用

3)调整 style

部分属性会触发其他属性的自动调整,例如 position: absolute/fixedfloat 的元素会被计算为 display: block

2.2 渲染对象与 DOM 元素的关系

  • 多对一:多个 DOM 节点可能不产生 RenderObject(如 <head>display: none 的元素);visibility: hidden 的元素仍在渲染树中,只是不可见
  • 一对多:复杂元素可能对应多个 RenderObject,如 <select> 有显示区、下拉列表、按钮三个渲染对象;文本换行时每一行也会新增 RenderObject
  • 位置不同:浮动和绝对定位元素的 RenderObject 会脱离正常文档流,在原位置留下占位框架

八、布局(Layout / Reflow)

渲染树构建完成后,浏览器需要为每个 RenderObject 计算 几何信息(位置、尺寸),这个过程称为 布局重排(Reflow)

布局从根 RenderObject(对应 <html> 元素)开始,递归遍历渲染树,为每个节点计算坐标和大小。

HTML 默认采用 流式布局(Flow Layout):大多数情况下只需一次从上到下、从左到右的遍历即可确定几何信息——靠后的元素通常不影响靠前元素的位置。例外情况包括表格布局、Flex/Grid 中的 auto 尺寸等,可能需要多轮计算。

坐标系以根 RenderObject 为原点,左上角为 (0, 0),尺寸等于 视口(Viewport) 大小。

触发重排的操作

类型 示例
DOM 变更 增删节点、修改文本
几何样式 widthheightmarginpaddingborder
布局上下文 displaypositionfloat
读取布局属性 offsetWidthgetBoundingClientRect() 强制同步布局

频繁交替读写布局属性会导致 布局抖动(Layout Thrashing)——浏览器被迫在读写之间反复同步布局计算。

九、绘制与合成

1、绘制(Paint)

布局完成后,浏览器遍历渲染树,将各节点绘制为位图或绘制指令列表。绘制按 绘制顺序(Paint Order) 进行,受 z-indexstacking context 等影响。

绘制通常分多个阶段:背景 → 边框 → 内容 → 阴影等,最终生成 绘制记录(Paint Record)

2、合成(Composite)

现代浏览器会将页面拆分为多个 合成层(Compositing Layer),由 合成线程(Compositor Thread) 在 GPU 上完成最终合成。

以下情况可能触发独立合成层:

  • transformopacity 动画
  • will-change: transform 提示
  • <video><canvas><iframe> 等元素
  • 3D transform(translateZ(0)

只改变合成层属性时,可跳过 Layout 和 Paint,直接 Composite。

十、完整流程回顾

  1. 客户端:用户输入 URL,浏览器解析并校验(协议、HSTS、安全检查)
  2. DNS 解析:浏览器缓存 → OS 缓存 → hosts → 本地 DNS 迭代查询,获取服务器 IP
  3. TCP 三次握手建立连接;HTTPS 还需 TLS 握手
  4. 浏览器发送 HTTP 请求;可选 103 Early Hints 提前预加载
  5. 服务器返回 HTTP 响应(HTML 及关联资源)
  6. 浏览器 流式解析 HTML,构建 DOM 树
  7. 并行 解析 CSS 构建 CSSOM 树;遇到 JavaScript 下载并执行(async/defer 可改变阻塞行为)
  8. 合并 DOM 与 CSSOM,构建 渲染树(排除非视觉节点和 display: none 元素)
  9. 布局:计算每个节点的几何信息
  10. 绘制:生成绘制指令
  11. 合成:GPU 将各层合成为最终画面,显示到屏幕

参考链接

微信打赏