跳到主要内容
预计阅读 35 分钟

导航流程 — 从按下回车到看见页面,中间到底发生了什么

用户在地址栏输入 URL 按下回车,到页面内容完整呈现在屏幕上,中间经历了数十个步骤、跨越了多个进程。这条链路是浏览器工作原理的”主干道”,理解它,后面所有章节的知识都能串起来。

📋 开篇自测:你已经知道多少?

  1. 从输入 URL 到页面显示,你能说出至少 5 个关键步骤吗?
  2. DNS 解析是在哪个进程中完成的?为什么需要 DNS 缓存?
  3. 什么是”提交导航”?浏览器进程和渲染进程是如何交接数据的?

一、导航流程全景图

在深入每个步骤之前,先建立一个完整的全景视图。整个导航流程可以分为七个阶段:

导航流程七大阶段

用户输入 → URL解析 → DNS解析 → TCP连接 → HTTP请求/响应

                   页面渲染完成 ← 提交导航 ← 响应处理

                    ┌───▼───┐
                    │ 完成! │
                    └───────┘

详细流程(跨进程协作):

   浏览器主进程          网络进程            渲染进程
   ┌──────────┐      ┌──────────┐      ┌──────────┐
   │1.用户输入 │      │          │      │          │
   │2.URL解析  │──────│→3.DNS解析│      │          │
   │          │      │ 4.TCP连接│      │          │
   │          │      │ 5.HTTP   │      │          │
   │          │◄─────│──6.响应  │      │          │
   │7.提交导航 │──────│──────────│──────│→8.解析   │
   │          │      │          │      │ 9.渲染   │
   │          │◄─────│──────────│──────│──10.完成  │
   └──────────┘      └──────────┘      └──────────┘

接下来逐一拆解。


二、第一阶段:用户输入与 URL 解析

2.1 地址栏的智能判断

当你在 Chrome 地址栏输入内容时,浏览器主进程中的 UI 线程首先要判断:这是一个 URL,还是一个搜索词?

判断规则大致如下:

  • 包含 . 且符合域名格式 → 作为 URL 处理
  • http://https:// 开头 → 明确是 URL
  • 其他情况 → 当作搜索关键词,拼接到默认搜索引擎的 URL 模板中

例如输入 github.com,浏览器会自动补全为 https://github.com;输入 浏览器原理,则会被转换为 https://www.google.com/search?q=浏览器原理

2.2 beforeunload 事件

如果当前页面有注册 beforeunload 事件处理函数,浏览器会在开始导航前先触发它。这是页面的最后一次机会来提醒用户”你有未保存的修改,确定要离开吗?”。

// 页面中的代码
window.addEventListener('beforeunload', (event) => {
  event.preventDefault();
  event.returnValue = ''; // Chrome需要这行才会显示提示
});

只有当用户确认离开(或页面没有注册该事件),导航才会继续。

2.3 URL 的组成结构

一个完整的 URL 包含多个部分,每个部分在导航流程中都有特定的用途:

URL 结构分解

https://www.example.com:443/path/page.html?key=value#section

  │        │           │    │              │          │
  │        │           │    │              │          └── Fragment(片段)
  │        │           │    │              │              不发送到服务器
  │        │           │    │              └── Query(查询参数)
  │        │           │    └── Path(路径)
  │        │           └── Port(端口, HTTPS默认443)
  │        └── Host(主机名, 需要DNS解析)
  └── Scheme(协议)

三、第二阶段:DNS 解析 — 把域名翻译成 IP 地址

3.1 为什么需要 DNS

计算机之间的通信基于 IP 地址,但人类记不住一长串数字。DNS(Domain Name System)就是互联网的”电话簿”,负责把人类友好的域名(如 github.com)翻译成机器友好的 IP 地址(如 140.82.121.4)。

3.2 DNS 解析的完整链路

DNS 解析并不是一步到位的,它有一套精密的多级缓存和递归查询机制:

DNS 解析查找顺序(逐级回退)

1. 浏览器 DNS 缓存
   │  命中? ──→ 直接返回 IP
   │  未命中 ↓
2. 操作系统 DNS 缓存 (如 /etc/hosts)
   │  命中? ──→ 直接返回 IP
   │  未命中 ↓
3. 路由器 DNS 缓存
   │  命中? ──→ 直接返回 IP
   │  未命中 ↓
4. ISP DNS 服务器 (如运营商的DNS)
   │  命中? ──→ 直接返回 IP
   │  未命中 ↓
5. 递归查询:
   根DNS服务器 → 顶级域服务器(.com) → 权威DNS服务器(example.com)

   └──→ 返回最终 IP 地址,并沿途缓存

每一级缓存都有 TTL(Time To Live),即缓存的有效期。TTL 到期后,下次查询会重新走递归流程。

3.3 DNS 对性能的影响

DNS 解析通常耗时 20~120 毫秒。对于首次访问一个新域名,这个时间是无法避免的。但浏览器提供了优化手段:

DNS 预解析(dns-prefetch):告诉浏览器”我稍后可能会访问这个域名,请提前解析”。

<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//api.example.com">

DNS over HTTPS(DoH):传统 DNS 查询是明文的,任何中间人都能看到你访问了什么网站。DoH 通过 HTTPS 加密 DNS 查询,提升了隐私性,但可能增加少量延迟。

🤔 想一想 如果一个页面引用了 10 个不同域名的资源(CDN、API、字体、统计等),DNS 解析会对首次加载产生多大的影响?你会如何优化?


四、第三阶段:TCP 连接 — 建立可靠的传输通道

4.1 三次握手

DNS 解析拿到了服务器的 IP 地址后,浏览器需要与服务器建立 TCP 连接。TCP 是面向连接的可靠传输协议,建立连接需要经过”三次握手”:

TCP 三次握手

  客户端(浏览器)                    服务器
       │                             │
       │──── SYN (seq=x) ──────────→│  第1次: 客户端发起连接请求
       │                             │
       │←── SYN+ACK (seq=y,ack=x+1)─│  第2次: 服务器确认并发起反向连接
       │                             │
       │──── ACK (ack=y+1) ────────→│  第3次: 客户端确认反向连接
       │                             │
       │     连接建立,可以传输数据     │
       │                             │

耗时: 1 RTT(第三次握手的 ACK 可以携带数据,因此严格来说是 1 RTT;部分资料计为 1.5 RTT,取决于是否将最后的 ACK 计入)

为什么是三次而不是两次?因为两次无法确认双方的收发能力都正常。第三次握手确保了服务器知道客户端已经收到了自己的确认。

4.2 TLS 握手(HTTPS)

如果使用 HTTPS(现代网站的标配),在 TCP 连接建立之后,还需要进行 TLS 握手来协商加密方式和交换密钥:

TLS 1.3 握手(简化)

  客户端                              服务器
     │                                  │
     │── ClientHello + 密钥共享参数 ───→│  客户端发送支持的加密套件
     │                                  │
     │←─ ServerHello + 密钥共享 + 证书 ─│  服务器选择加密套件,发送证书
     │                                  │
     │   客户端验证证书,计算密钥         │
     │── Finished ─────────────────────→│  加密通道建立
     │                                  │

TLS 1.3: 1 个 RTT (比TLS 1.2的2个RTT快了一轮)

4.3 连接复用与 HTTP/2

TCP + TLS 握手是昂贵的。对于 HTTP/1.1,浏览器通常会对同一域名建立 6 个并行连接来提高并发性。HTTP/2 的一大优势是多路复用(Multiplexing):在同一个 TCP 连接上同时传输多个请求和响应,彻底消除了连接数的限制。

HTTP/1.1 vs HTTP/2 连接模型

HTTP/1.1:
┌─────┐  连接1 → 请求A → 响应A
│     │  连接2 → 请求B → 响应B
│浏览器│  连接3 → 请求C → 响应C   (最多6个并行连接/域名)
│     │  连接4 → 请求D → 响应D
└─────┘  连接5 → 请求E → 响应E
         连接6 → 请求F → 响应F

HTTP/2:
┌─────┐                      ┌──────┐
│     │  单一连接 ═══════════ │      │
│浏览器│  ├ 流1: 请求A/响应A  │服务器│
│     │  ├ 流2: 请求B/响应B  │      │
└─────┘  ├ 流3: 请求C/响应C  │      │
         ├ 流4: 请求D/响应D  └──────┘
         └ ...(无并发限制)

五、第四阶段:HTTP 请求与响应

5.1 构造 HTTP 请求

TCP(或 TLS)连接建立后,网络进程开始构造 HTTP 请求报文:

HTTP 请求报文结构

┌─ 请求行 ──────────────────────────────┐
│ GET /index.html HTTP/1.1              │
├─ 请求头 ──────────────────────────────┤
│ Host: www.example.com                 │
│ User-Agent: Mozilla/5.0 ...          │
│ Accept: text/html                     │
│ Accept-Encoding: gzip, br             │
│ Accept-Language: zh-CN,zh;q=0.9       │
│ Cookie: session_id=abc123             │
│ Connection: keep-alive                │
├─ 空行 ────────────────────────────────┤
│                                       │
├─ 请求体 (GET请求通常为空) ──────────────┤
│                                       │
└───────────────────────────────────────┘

5.2 服务器处理与响应

服务器收到请求后进行处理(路由匹配、数据库查询、模板渲染等),然后返回 HTTP 响应:

HTTP 响应报文结构

┌─ 状态行 ──────────────────────────────┐
│ HTTP/1.1 200 OK                       │
├─ 响应头 ──────────────────────────────┤
│ Content-Type: text/html; charset=utf-8│
│ Content-Length: 3256                   │
│ Content-Encoding: gzip                │
│ Cache-Control: max-age=3600           │
│ Set-Cookie: theme=dark                │
├─ 空行 ────────────────────────────────┤
│                                       │
├─ 响应体 ──────────────────────────────┤
│ <!DOCTYPE html>                       │
│ <html>...                             │
└───────────────────────────────────────┘

5.3 关键状态码与重定向

网络进程收到响应后,首先检查状态码:

状态码含义浏览器行为
200成功继续处理响应体
301永久重定向读取 Location 头,重新发起请求
302临时重定向同上,但不缓存重定向
304资源未修改使用本地缓存
404资源不存在显示错误页面
500服务器内部错误显示错误页面

如果是 301 或 302,浏览器会从响应头中读取 Location 字段,然后从 DNS 解析阶段重新开始整个流程。这就是为什么过多的重定向会严重拖慢页面加载速度。

5.4 响应类型判断

网络进程还需要根据 Content-Type 头判断响应体的类型:

  • text/html → 交给渲染进程处理
  • application/octet-stream → 触发下载
  • application/pdf → 使用 PDF 查看器打开(如果有的话)

只有当 Content-Type 指明是 HTML 文档时,流程才会进入下一阶段——提交导航。


六、第五阶段:提交导航 — 进程间的交接仪式

这是整个导航流程中最关键、也最容易被忽视的环节。它涉及浏览器主进程和渲染进程之间的一次正式”交接”。

6.1 提交导航的流程

提交导航详细流程

浏览器主进程                          渲染进程
     │                                  │
     │  1. "准备好了,我要给你发数据"     │
     │──── CommitNavigation ──────────→│
     │                                  │
     │                      2. 渲染进程确认收到
     │←─── 确认消息 ──────────────────│
     │                                  │
     │  3. 浏览器更新UI:                │  4. 渲染进程开始接收数据
     │     - 地址栏URL更新              │     并开始解析HTML
     │     - 安全锁图标显示             │
     │     - 标签页标题更新             │
     │     - 前进/后退列表更新          │
     │                                  │
     │                      5. 渲染完成
     │←─── DidFinishLoad ─────────────│
     │                                  │
     │  6. 标签页loading图标停止旋转    │
     │                                  │

6.2 为什么需要”提交”这个步骤

你可能会问:为什么不直接把数据扔给渲染进程?因为在导航流程中,浏览器主进程需要协调多件事情:

  1. 安全检查:在把响应数据交给渲染进程之前,浏览器要确认该站点没有被标记为恶意站点(Safe Browsing 检查)
  2. CORB(Cross-Origin Read Blocking)检查:确保跨域资源不会被错误地加载到渲染进程中
  3. 渲染进程选择:根据站点隔离策略,决定是复用已有的渲染进程还是创建新的

七、第六阶段:页面解析与渲染(概览)

提交导航完成后,渲染进程开始接收 HTML 数据流(注意:不需要等到全部数据到达才开始解析,而是边接收边解析)。

渲染的详细流程将在第 3 章和第 4 章深入讲解,这里给出一个简化的概览:

渲染流程概览

HTML字节流


┌─────────┐    ┌──────────┐    ┌──────────┐
│HTML解析  │───→│ DOM树     │    │ CSSOM树   │
│(主线程)  │    │          │    │          │
└─────────┘    └────┬─────┘    └────┬─────┘
                    │               │
                    └───────┬───────┘
                            │ 合并

                    ┌──────────────┐
                    │   渲染树      │
                    │ (Render Tree)│
                    └──────┬───────┘


                    ┌──────────────┐
                    │    布局       │
                    │  (Layout)    │
                    └──────┬───────┘


                    ┌──────────────┐
                    │    绘制       │
                    │  (Paint)     │
                    └──────┬───────┘


                    ┌──────────────┐
                    │   合成显示    │
                    │ (Composite)  │
                    └──────────────┘

7.1 关键资源对渲染的阻塞

在解析 HTML 的过程中,并不是所有资源都会被平等对待:

CSS 阻塞渲染:浏览器必须等到 CSSOM 构建完成才能进行渲染。如果 CSS 文件很大或加载很慢,用户会看到白屏。

JavaScript 阻塞解析:当 HTML 解析器遇到 <script> 标签时,它必须暂停解析,等待脚本下载和执行完毕后才能继续。这是因为 JavaScript 可能会通过 document.write() 修改 DOM 结构。

阻塞行为示意

HTML解析: ████████░░░░░░░░░████████████████
                 ↑          ↑
                 │          └── 脚本执行完毕,解析恢复
                 └── 遇到<script>标签,解析暂停

解决方案:
<script defer src="app.js">   → 异步下载,HTML解析完后执行
<script async src="analytics.js"> → 异步下载,下载完立即执行

7.2 预加载扫描器(Preload Scanner)

浏览器的工程师们当然不会坐视 JavaScript 阻塞带来的性能损失。Chrome 有一个预加载扫描器,它在主线程的 HTML 解析器暂停时,会继续向前扫描 HTML,提前发现后续需要加载的资源(CSS、JavaScript、图片等),并启动下载。

预加载扫描器工作原理

主线程:    [HTML解析] [暂停等JS] [恢复解析]

预加载扫描器:             [继续扫描HTML,发现后续资源]

网络进程:                     [提前下载CSS][提前下载图片]

这个优化对页面加载性能的提升是巨大的。这也是为什么你不应该用 JavaScript 动态生成 <link><script> 标签来加载关键资源——预加载扫描器无法发现它们。


八、第七阶段:完成加载

8.1 DOMContentLoaded vs load

当所有同步 JavaScript 执行完毕、DOM 树构建完成后,浏览器触发 DOMContentLoaded 事件。当页面所有资源(包括图片、样式表、iframe)都加载完毕后,触发 load 事件。

页面加载事件时序

HTML接收开始        DOM树构建完成        所有资源加载完毕
     │                   │                     │
     ▼                   ▼                     ▼
─────┼───────────────────┼─────────────────────┼──→ 时间
     │                   │                     │
     │    HTML解析+JS执行 │   图片/字体等加载    │
     │                   │                     │
                  DOMContentLoaded           load
                         │                     │
                    适合执行DOM操作         适合统计性能指标

8.2 首次内容绘制(FCP)和最大内容绘制(LCP)

在用户体验层面,有两个关键时刻:

  • FCP(First Contentful Paint):浏览器第一次在屏幕上绘制出任何内容(文字、图片、SVG 等)的时刻
  • LCP(Largest Contentful Paint):页面中最大的内容元素完成渲染的时刻

这两个指标是 Google 的 Core Web Vitals 的核心组成部分,也是衡量用户”感知加载速度”的关键指标。


九、完整时序:一次导航的生命周期

把所有阶段串联起来,一次完整导航的时间轴如下:

完整导航时序(假设首次访问 HTTPS 站点)

时间 →

├─ 用户输入 + URL解析                    ~1ms

├─ DNS解析                               ~20-120ms
│  (如果命中缓存则 ~0ms)

├─ TCP三次握手                            ~1 RTT (~30-100ms)

├─ TLS握手                                ~1 RTT (TLS 1.3)

├─ HTTP请求发送                           ~0.5 RTT

├─ 服务器处理                              ~50-500ms (取决于后端)

├─ HTTP响应首字节 (TTFB)                   ~服务器处理时间 + 0.5 RTT

├─ 响应体下载                              取决于文件大小和带宽

├─ 提交导航                               ~几ms

├─ HTML解析 + DOM构建 + CSSOM构建          ~取决于文档复杂度

├─ JavaScript执行                          ~取决于JS体积和复杂度

├─ 布局 + 绘制 + 合成                      ~取决于页面元素数量

├─ FCP (First Contentful Paint)            理想: <1.8s

├─ LCP (Largest Contentful Paint)          理想: <2.5s

└─ load 事件                               页面完全加载

🤔 想一想 在上面的时序中,哪些阶段是可以通过前端优化来缩短的?哪些阶段需要后端或运维配合?


十、导航的特殊场景

10.1 同站点导航(Same-site Navigation)

当用户从 a.example.com/page1 导航到 a.example.com/page2 时,浏览器可以复用已有的渲染进程和 TCP 连接,跳过很多步骤。这就是为什么同站点的页面跳转通常比跨站点快得多。

10.2 Service Worker 拦截

如果目标页面注册了 Service Worker,导航流程会多出一个步骤:网络进程在发起真正的网络请求之前,会先检查是否有 Service Worker 可以处理这个请求。如果有,Service Worker 可以直接从缓存中返回响应,完全不需要访问网络。

Service Worker 拦截流程

正常流程:   URL → DNS → TCP → HTTP → 响应
SW拦截流程: URL → 检查SW → SW返回缓存 → 响应 (跳过网络!)

10.3 浏览器缓存命中

如果请求的资源被浏览器缓存命中(强缓存),网络进程甚至不需要发起网络请求:

  • 强缓存Cache-Control: max-age=3600):直接从磁盘/内存缓存返回,状态码 200(from cache)
  • 协商缓存ETag / Last-Modified):发送条件请求到服务器,如果资源未变,服务器返回 304,浏览器使用本地缓存

十一、本章知识脉络总结

导航流程知识地图

从URL到页面
├── 用户输入阶段
│   ├── URL vs 搜索词判断
│   ├── beforeunload事件
│   └── URL结构解析

├── 网络阶段
│   ├── DNS解析 (多级缓存 + 递归查询)
│   ├── TCP三次握手
│   ├── TLS握手 (HTTPS)
│   ├── HTTP请求/响应
│   └── 状态码处理 + 重定向

├── 提交导航
│   ├── 安全检查 (Safe Browsing)
│   ├── 渲染进程选择
│   └── 进程间数据交接

├── 渲染阶段 (概览)
│   ├── HTML解析 → DOM树
│   ├── CSS解析 → CSSOM树
│   ├── JS执行 (阻塞解析)
│   ├── 预加载扫描器
│   └── 布局 → 绘制 → 合成

├── 加载完成
│   ├── DOMContentLoaded
│   ├── load
│   ├── FCP / LCP
│   └── 性能指标

└── 特殊场景
    ├── 同站点导航 (进程复用)
    ├── Service Worker 拦截
    └── 浏览器缓存命中

📝 结尾自测:检验你的学习成果

  1. 完整描述从输入 URL 到页面显示的七个主要阶段。
  2. DNS 解析有哪些层级的缓存?一个完整的递归查询经过哪些服务器?
  3. TCP 三次握手的每一步各确认了什么?为什么不能只握手两次?
  4. 什么是”提交导航”?它在浏览器主进程和渲染进程之间起什么作用?
  5. <script> 标签的 deferasync 属性分别是如何影响 HTML 解析的?预加载扫描器又是如何弥补阻塞造成的性能损失的?

下一章预告:导航流程走到”提交导航”之后,数据就交给了渲染进程。下一章我们将深入渲染进程内部,详细拆解 HTML 解析器如何一个字节一个字节地构建 DOM 树,CSS 如何被解析和计算,以及布局树是如何生成的。

购买课程解锁全部内容

前端进阶第一课:11 章掌握浏览器核心

¥29.90