6503 words
33 minutes
从在浏览器输入网址到页面完全加载完成,这中间发生了什么?

“从在浏览器输入网址到页面完全加载完成,这中间发生了什么?”

这是前端面试中最经典、也最能拉开候选人差距的一道题。表面看是一个简单的过程描述,但深入挖掘可以涉及操作系统、网络协议、浏览器架构、渲染引擎、JS 引擎、安全机制、性能优化等几乎所有计算机基础知识。

回答这道题的层次差异:

  • 初级回答:DNS → TCP → HTTP → 渲染(5 句话答完)
  • 中级回答:能讲清楚 TCP 三次握手、HTTPS 加密、关键渲染路径
  • 高级回答:能讲清楚 QUIC、HTTP/2、TLS 1.3、浏览器多进程架构、合成线程、Speculative Parsing 等

本文按照高级回答的标准,从你按下键盘那一刻开始,把整个过程拆解成 15 个阶段,每个阶段都尽量写到底。


阶段总览#

阶段关键动作涉及层级
1键盘输入硬件 + 操作系统
2URL 解析与补全浏览器
3安全检查与拦截浏览器
4检查缓存浏览器
5DNS 查询应用层
6建立 TCP 连接传输层
7TLS 握手(HTTPS)表示层
8发送 HTTP 请求应用层
9服务器处理与响应后端
10浏览器接收响应浏览器
11解析 HTML 构建 DOM渲染引擎
12解析 CSS 构建 CSSOM渲染引擎
13执行 JavaScriptJS 引擎
14渲染:Layout → Paint → Composite渲染引擎 + GPU
15触发各种事件,加载完成浏览器

下面逐个展开。


阶段 1:键盘输入到浏览器接收字符#

这一步几乎所有教程都跳过,但其实信息量很大。

1.1 硬件信号#

当你按下键盘上的 G 键时:

  1. 键盘内的**键盘控制器(Keyboard Controller)**检测到按键闭合,产生扫描码(Scan Code)
  2. 通过 USB/PS2/蓝牙协议传输到主板
  3. 主板**中断控制器(PIC/APIC)**触发硬件中断(IRQ 1)
  4. CPU 暂停当前任务,跳转到键盘中断处理程序

1.2 操作系统处理#

  1. 内核驱动接收扫描码,转换成键码(Key Code)
  2. 通过 OS 的输入子系统(如 Linux 的 evdev、Windows 的 RawInput)传递给当前焦点窗口
  3. 浏览器进程通过事件循环接收到键盘事件

1.3 浏览器处理#

  1. 浏览器的 UI 进程接收到键盘事件
  2. 判断焦点在地址栏(Omnibox),将字符送入地址栏组件
  3. 触发自动补全:从历史记录、书签、搜索引擎建议中匹配
  4. 当用户按下 Enter 键,开始真正的导航流程

附加知识:Chrome 的地址栏(Omnibox)实际上是一个非常复杂的组件,它在你输入时会预解析 URL、预连接 DNS、甚至预渲染页面。这就是为什么有时候你输入到一半,目标页面就已经”准备好了”——这是 Chrome 的 Preconnect / Prerender 优化机制。


阶段 2:URL 解析与补全#

浏览器拿到地址栏的字符串后,第一步是判断它是 URL 还是搜索词。

2.1 判断逻辑#

输入: "github.com"
├─ 是否包含空格? → 否
├─ 是否符合域名格式? → 是
├─ 是否能解析? → 尝试 DNS
└─ 当作 URL 处理
输入: "如何学习前端"
├─ 是否包含空格? → 是
├─ 是否符合域名格式? → 否
└─ 当作搜索词,跳转到默认搜索引擎

2.2 URL 补全#

对于”github.com”这样的输入,浏览器会自动补全:

  • 协议:默认补 https://(现代浏览器优先 HTTPS)
  • 路径:默认补 /
  • 端口:HTTPS 默认 443,HTTP 默认 80

最终变成 https://github.com/

2.3 URL 编码#

URL 中包含非 ASCII 字符(如中文)时,需要进行 percent-encoding

原始: https://example.com/搜索?q=前端
编码: https://example.com/%E6%90%9C%E7%B4%A2?q=%E5%89%8D%E7%AB%AF

域名部分如果是中文,还要进行 Punycode 转换:

中文域名: 中国.cn
Punycode: xn--fiqs8s.cn

附加知识:URL 的标准格式是 scheme://user:password@host:port/path?query#fragment。其中 #fragment 部分不会发送到服务器,仅供前端使用——这就是单页应用(SPA)早期使用 Hash 路由的原理。


阶段 3:安全检查与拦截#

在真正发起网络请求之前,浏览器会进行一系列安全检查。

3.1 HSTS(HTTP Strict Transport Security)#

浏览器维护一个 HSTS Preload List,对列表中的域名强制使用 HTTPS。如果你输入 http://github.com,浏览器会直接在本地改写为 https://github.com根本不会发送 HTTP 请求

3.2 Safe Browsing#

Chrome 维护一个恶意网站黑名单(Google Safe Browsing),如果目标 URL 在黑名单中,会显示红色警告页面。

3.3 内容安全策略(CSP)#

如果当前页面是从其他页面跳转来的,浏览器会检查源页面的 CSP 头,判断是否允许跳转。

3.4 混合内容检测#

HTTPS 页面中如果加载 HTTP 资源,浏览器会拦截或警告(Mixed Content Blocking)。


阶段 4:检查缓存#

在发起网络请求之前,浏览器会按顺序检查多级缓存:

1. Memory Cache(内存缓存)
↓ 未命中
2. Service Worker Cache(PWA 缓存)
↓ 未命中
3. HTTP Cache(磁盘缓存)
↓ 未命中
4. Push Cache(HTTP/2 推送缓存)
↓ 未命中
5. 发起网络请求

4.1 强缓存 vs 协商缓存#

强缓存:浏览器直接使用本地副本,不向服务器发请求。

Cache-Control: max-age=3600, public
Expires: Wed, 28 May 2026 12:00:00 GMT

协商缓存:浏览器发请求,服务器决定是否使用本地副本。

If-Modified-Since: Wed, 28 May 2026 10:00:00 GMT
If-None-Match: "5d8c72a5edda3-1432"

服务器若返回 304 Not Modified,浏览器使用本地缓存。

4.2 缓存优先级#

Cache-Control > Expires(前者是 HTTP/1.1 标准,后者是 HTTP/1.0 遗留)。

ETag > Last-Modified(前者更精确,后者按秒计算可能有误差)。

附加知识:Chrome 的 Memory Cache 存活时间很短(通常只有一次会话),Disk Cache 则可以长期保留。预加载(preload)的资源默认会进 Memory Cache,这是为什么 preload 比 prefetch 优先级更高。


阶段 5:DNS 查询#

如果缓存未命中,浏览器需要把域名解析成 IP 地址。

5.1 DNS 查询的多级缓存#

1. 浏览器 DNS 缓存(chrome://net-internals/#dns)
↓ 未命中
2. 操作系统 DNS 缓存(hosts 文件 + 系统缓存)
↓ 未命中
3. 路由器 DNS 缓存
↓ 未命中
4. ISP(运营商)DNS 服务器
↓ 未命中
5. 递归查询根 DNS → TLD DNS → 权威 DNS

5.2 递归查询过程#

以查询 www.github.com 为例:

  1. 客户端 → 本地 DNS 服务器:你知道 www.github.com 的 IP 吗?
  2. 本地 DNS → 根 DNS(.:你知道吗?根 DNS:不知道,去问 .com 服务器
  3. 本地 DNS → .com TLD 服务器:你知道吗?TLD:不知道,去问 github.com 的权威服务器
  4. 本地 DNS → github.com 权威服务器:你知道吗?权威:知道,IP 是 140.82.114.4
  5. 本地 DNS → 客户端:IP 是 140.82.114.4

5.3 DNS 记录类型#

类型含义
AIPv4 地址
AAAAIPv6 地址
CNAME别名
MX邮件服务器
TXT文本(常用于域名验证)
NS权威 DNS 服务器

5.4 DNS over HTTPS / DNS over TLS#

传统 DNS 查询是明文的,存在被劫持的风险。现代浏览器支持:

  • DoH(DNS over HTTPS):DNS 查询通过 HTTPS 发送
  • DoT(DNS over TLS):DNS 查询通过 TLS 加密

Chrome、Firefox 默认开启 DoH。

5.5 DNS Prefetch#

可以在 HTML 中提前声明:

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

浏览器会在空闲时间预解析 DNS,减少后续请求的延迟。

附加知识:DNS 默认基于 UDP 53 端口,响应大小超过 512 字节时会回退到 TCP。EDNS(Extension Mechanisms for DNS)扩展了 UDP 包大小限制。


阶段 6:建立 TCP 连接(三次握手)#

拿到 IP 地址后,浏览器需要和服务器建立 TCP 连接。

6.1 三次握手#

客户端 服务器
│ │
├──── SYN (seq=x) ────────────────→ │ 第一次握手
│ │
│ ←─── SYN+ACK (seq=y, ack=x+1) ────┤ 第二次握手
│ │
├──── ACK (ack=y+1) ──────────────→ │ 第三次握手
│ │
│ 连接建立完成 │

6.2 为什么是三次而不是两次?#

如果只有两次握手,无法确认客户端的接收能力

  • 第一次握手:服务器知道客户端能发
  • 第二次握手:客户端知道服务器能收能发
  • 第三次握手:服务器知道客户端能收

少了第三次,服务器就不知道客户端是否还活着,可能向已经离线的客户端发送大量数据。

6.3 为什么不是四次?#

四次握手可以合并为三次——第二次握手中,服务器同时完成 ACK 和 SYN,无需分开发送。

6.4 TCP 连接的代价#

三次握手 = 1 个 RTT(Round Trip Time,往返时间)。

如果服务器在美国、客户端在中国,RTT 约 200ms,仅 TCP 握手就要消耗 200ms。这是 HTTP/3 改用 QUIC(基于 UDP)的核心动机。

6.5 TCP Fast Open(TFO)#

TFO 是一个优化,允许在 SYN 包中携带数据,减少一次往返。但需要服务器支持,且第一次连接仍需完整握手。


阶段 7:TLS 握手(HTTPS 加密)#

HTTPS = HTTP + TLS。TCP 连接建立后,还需要进行 TLS 握手。

7.1 TLS 1.2 完整握手(2 RTT)#

客户端 服务器
│ │
├── Client Hello ─────────────────→ │
│ (支持的加密套件、随机数) │
│ │
│ ←── Server Hello ─────────────────┤
│ (选定的加密套件、随机数、证书) │
│ │
├── 验证证书 │
├── 生成 Pre-Master Secret │
├── Client Key Exchange ──────────→ │
│ (用服务器公钥加密的 │
│ Pre-Master Secret) │
│ │
├── Finished (加密) ──────────────→ │
│ │
│ ←── Finished (加密) ──────────────┤
│ │
│ 开始加密通信 │

7.2 TLS 1.3 握手(1 RTT)#

TLS 1.3 大幅简化了握手过程:

  • 移除了不安全的加密套件(RSA Key Exchange、SHA-1、MD5 等)
  • 客户端在第一个消息中就发送密钥交换参数
  • 只需 1 RTT 完成握手
  • 支持 0-RTT 恢复(对已访问过的网站,可以在第一个数据包中就发送 HTTP 请求)

7.3 证书验证#

浏览器收到服务器证书后,进行以下验证:

  1. 域名匹配:证书中的 CN/SAN 字段是否匹配当前域名
  2. 有效期:是否在 Not BeforeNot After 之间
  3. 证书链:从根 CA 到当前证书是否完整
  4. 吊销状态:通过 CRL(证书吊销列表)或 OCSP(在线证书状态协议)查询
  5. CT 日志:Chrome 要求证书在 Certificate Transparency 日志中

7.4 加密体系#

TLS 同时使用两种加密:

  • 非对称加密(如 RSA、ECDHE):用于密钥交换
  • 对称加密(如 AES-256-GCM、ChaCha20-Poly1305):用于数据加密

为什么混用?因为非对称加密慢、对称加密快。用非对称加密安全地交换一个对称密钥,之后用对称密钥加密数据。

7.5 SNI(Server Name Indication)#

一个 IP 可能托管多个 HTTPS 网站(如 Cloudflare),服务器需要知道客户端要访问哪个域名才能返回正确的证书。SNI 扩展让客户端在 Client Hello 中携带域名。

但 SNI 是明文的,会泄露访问目标。**ESNI / ECH(Encrypted Client Hello)**是解决这个问题的新标准。


阶段 8:发送 HTTP 请求#

加密通道建立后,浏览器构造 HTTP 请求。

8.1 请求格式#

GET / HTTP/1.1
Host: github.com
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml,...
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate, br
Cookie: _octo=GH1.1.xxx; user_session=xxx
Connection: keep-alive
Upgrade-Insecure-Requests: 1

8.2 HTTP 各版本对比#

版本年份关键特性
HTTP/0.91991只支持 GET
HTTP/1.01996支持 POST、HEAD、状态码、头部
HTTP/1.11997长连接(keep-alive)、管线化(pipelining)、Host 头
HTTP/22015二进制分帧、多路复用、头部压缩(HPACK)、服务端推送
HTTP/32022基于 QUIC(UDP),消除队头阻塞

8.3 HTTP/2 的多路复用#

HTTP/1.1 一个 TCP 连接同一时间只能处理一个请求(管线化也有队头阻塞问题)。HTTP/2 在单个 TCP 连接上可以并发多个流(Stream),每个流可以独立传输请求/响应。

8.4 HTTP/3 与 QUIC#

HTTP/2 仍基于 TCP,存在TCP 层队头阻塞——一个包丢失会阻塞所有流。HTTP/3 改用 QUIC(基于 UDP),在传输层实现流的独立性,丢包只影响单个流。

QUIC 还内置了 TLS 1.3,握手只需 1 RTT,0-RTT 恢复时甚至 0 RTT

浏览器在发请求时会自动附带 Cookie:

  • 作用域:由 Domain + Path 决定
  • 属性:HttpOnly、Secure、SameSite、Max-Age
  • 大小限制:单个 Cookie ≤ 4KB

SameSite 属性是防 CSRF 的关键:

  • Strict:跨站完全不发送
  • Lax:跨站只在顶级导航时发送(默认值)
  • None:跨站发送,但必须配合 Secure

阶段 9:服务器处理与响应#

请求到达服务器后的处理流程(以 Node.js + Nginx 为例):

9.1 反向代理#

请求首先到达 Nginx / CDN 边缘节点

  • 负载均衡:根据策略(轮询、加权、IP Hash)转发到后端
  • 缓存:边缘节点缓存静态资源
  • 限流 / 防火墙:拦截恶意请求
  • SSL 终结:在边缘节点解密,后端用 HTTP

9.2 应用服务器处理#

后端框架(Express、Koa、Spring、Django 等)的处理流程:

  1. 路由匹配
  2. 中间件链(鉴权、日志、参数解析)
  3. 业务逻辑(查询数据库、调用其他服务)
  4. 模板渲染(如果是 SSR)
  5. 构造响应

9.3 数据库查询#

应用服务器 → 数据库连接池 → 数据库引擎 → 缓存(Redis)→ 磁盘

很多前端面试题会延伸问到:

  • N+1 查询问题:循环中查询数据库导致大量请求
  • 索引:B+ 树、Hash 索引的差异
  • 事务隔离级别:读未提交、读已提交、可重复读、串行化

9.4 响应格式#

HTTP/1.1 200 OK
Date: Wed, 28 May 2026 12:00:00 GMT
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Content-Length: 142857
Cache-Control: max-age=0, private
Set-Cookie: _gh_sess=xxx; Path=/; Secure; HttpOnly; SameSite=Lax
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: deny
Content-Security-Policy: default-src 'none'; ...
<!DOCTYPE html>
<html>...

9.5 状态码#

范围含义常见
1xx信息性100 Continue, 101 Switching Protocols
2xx成功200 OK, 201 Created, 204 No Content, 206 Partial Content
3xx重定向301 永久, 302 临时, 304 Not Modified, 307/308 严格重定向
4xx客户端错误400 Bad Request, 401 未认证, 403 禁止, 404 Not Found, 429 限流
5xx服务器错误500 内部错误, 502 Bad Gateway, 503 不可用, 504 网关超时

附加知识301302 都是重定向,但 SEO 含义不同。301 表示永久重定向,搜索引擎会更新索引;302 表示临时重定向,搜索引擎保留原 URL。


阶段 10:浏览器接收响应#

浏览器接收响应是一个流式过程——不是等全部数据到达才开始处理,而是边接收边解析。

10.1 内容协商#

根据 Content-Type 决定如何处理:

  • text/html → HTML 解析器
  • application/json → JSON 数据
  • image/* → 图像解码器
  • application/octet-stream → 下载

10.2 内容解压#

如果 Content-Encodinggzipbr(Brotli),浏览器先解压。Brotli 压缩率比 gzip 高 20-30%。

10.3 字符编码#

根据 Content-Type 中的 charset 或 HTML 中的 <meta charset> 决定字符编码。现代网页几乎都用 UTF-8。


阶段 11:解析 HTML 构建 DOM 树#

浏览器拿到 HTML 后,开始构建 DOM 树

11.1 解析步骤#

字节流 → 字符流 → 词法分析(Tokens)→ 语法分析(Nodes)→ DOM 树

例如:

<html>
<body>
<h1>Hello</h1>
</body>
</html>

构建的 DOM 树:

Document
└─ html
└─ body
└─ h1
└─ "Hello"

11.2 流式解析#

HTML 解析器是流式的——边接收字节边解析,不需要等待全部下载。

11.3 遇到外部资源#

  • <link rel="stylesheet">:异步下载 CSS,不阻塞 DOM 解析,但阻塞渲染
  • <script>:默认阻塞 HTML 解析(边下载边阻塞)
  • <script async>:异步下载,下载完立即执行(阻塞解析)
  • <script defer>:异步下载,DOM 解析完成后按顺序执行
  • <script type="module">:默认 defer 行为
  • <img>:异步下载,不阻塞解析也不阻塞渲染

11.4 预解析器(Speculative Parser)#

当主解析器遇到 <script> 被阻塞时,预解析器继续向后扫描 HTML,提前下载后续的 CSS、JS、图片资源。这是现代浏览器的重要优化。

11.5 容错机制#

HTML 解析器非常宽容

  • 未闭合标签自动闭合
  • 错误嵌套自动修正
  • 缺少根元素自动添加

这是 HTML 和 XML 最大的差异——XML 严格、HTML 宽松。


阶段 12:解析 CSS 构建 CSSOM#

CSS 解析与 HTML 解析并行进行。

12.1 解析过程#

CSS 文本 → Tokens → 规则(Rules)→ CSSOM 树

CSSOM(CSS Object Model)是一个树形结构,反映 CSS 规则的层级关系。

12.2 为什么 CSS 阻塞渲染?#

如果先渲染再加载 CSS,会出现 FOUC(Flash of Unstyled Content)——用户先看到无样式的丑陋页面,然后突然变样。

浏览器选择:有 CSS 才渲染,没 CSS 不渲染

12.3 为什么 CSS 阻塞 JS?#

如果 JS 在 CSS 加载完成前执行,可能读到错误的样式值(如 getComputedStyle)。所以浏览器规定:JS 必须等待之前的 CSS 加载完成

这是经典面试题”CSS 阻塞 JS”的根源。

12.4 优化策略#

  • 把关键 CSS 内联到 <head>
  • 非关键 CSS 用 media 属性异步加载:
    <link rel="stylesheet" href="print.css" media="print">
  • 使用 <link rel="preload"> 提前加载

阶段 13:执行 JavaScript#

13.1 JS 引擎工作流程(以 V8 为例)#

源代码
Parser(解析器)→ AST(抽象语法树)
Ignition(解释器)→ Bytecode(字节码)
执行(同时收集类型反馈)
TurboFan(优化编译器)→ 优化后的机器码
(如果假设失败)→ Deoptimization → 回退到字节码

13.2 单线程与事件循环#

JS 是单线程的,但浏览器是多线程的。事件循环(Event Loop)的核心机制:

┌─────────────────────────┐
│ 调用栈(Call Stack) │ ← 同步代码
└─────────────────────────┘
栈空时取微任务
┌─────────────────────────┐
│ 微任务队列(Microtask) │ ← Promise.then, queueMicrotask, MutationObserver
└─────────────────────────┘
微任务全部清空
┌─────────────────────────┐
│ 宏任务队列(Macrotask) │ ← setTimeout, setInterval, I/O, UI 事件
└─────────────────────────┘
取一个宏任务执行
渲染
循环

13.3 内存管理#

  • 新生代(Young Generation):Scavenge 算法,对象生存期短
  • 老生代(Old Generation):Mark-Sweep + Mark-Compact,对象生存期长
  • 隐藏类(Hidden Class):V8 的对象优化机制,对象属性顺序不一致会导致性能下降
  • 内联缓存(IC):缓存属性访问的位置,加速属性查找

13.4 操纵 DOM#

JS 通过 DOM API 操纵页面:

const el = document.querySelector('.box')
el.style.color = 'red'

每次 DOM 操作都可能触发重排(Reflow)重绘(Repaint)——这是性能优化的重点。


阶段 14:渲染 - Layout、Paint、Composite#

DOM 和 CSSOM 都准备好后,进入渲染管线。

14.1 渲染管线总览#

DOM + CSSOM
Style(样式计算)
Layout(布局,又叫 Reflow)
Paint(绘制)
Composite(合成)
GPU 显示

14.2 Style:构建 Render Tree#

合并 DOM 和 CSSOM,计算每个节点的最终样式

注意:display: none 的节点不会进入 Render Tree(不占空间),但 visibility: hidden(占空间)。

14.3 Layout:计算位置和大小#

遍历 Render Tree,计算每个节点在视口中的精确坐标和尺寸

触发 Layout(也叫 Reflow)的操作:

  • 修改 DOM 结构(增删节点)
  • 改变几何属性(width, height, padding, margin, top, left, font-size)
  • 读取触发同步布局的属性(offsetWidth, offsetHeight, getBoundingClientRect)
  • 改变窗口大小

14.4 Paint:绘制像素#

把 Layout 结果转换成屏幕上的像素——填充颜色、绘制边框、渲染文字、绘制图片。

只触发 Paint 不触发 Layout 的操作:

  • color、background-color
  • visibility
  • outline

14.5 Composite:图层合成#

现代浏览器把页面分成多个图层(Layer),由 GPU 并行绘制然后合成。

触发新图层的条件:

  • position: fixed
  • transform: translateZ(0)will-change: transform
  • 3D 变换
  • video、canvas、iframe
  • 滤镜(filter)

只触发 Composite 不触发 Layout/Paint 的属性:

  • transform
  • opacity

这就是为什么动画推荐用 transformopacity——它们只走合成线程,不走主线程,性能极高。

14.6 渲染线程架构#

Chrome 的渲染进程包含多个线程:

线程作用
主线程(Main)解析 HTML/CSS、执行 JS、Layout、Paint
合成线程(Compositor)合成图层、处理滚动
光栅线程(Raster)把图层栅格化为位图
Worker 线程Web Worker、Service Worker

为什么 transform 动画不卡? 因为它直接在合成线程执行,即使主线程被 JS 阻塞,动画依然流畅

14.7 60 FPS 与 16.67ms#

显示器一般 60Hz 刷新,每帧时间预算 = 1000ms / 60 ≈ 16.67ms

如果 JS 执行 + Layout + Paint 超过 16.67ms,会掉帧,用户感觉卡顿。


阶段 15:事件触发与加载完成#

整个加载过程触发一系列事件:

15.1 关键事件时间线#

导航开始
DOMContentLoaded ← DOM 解析完成(不等图片/样式)
load ← 所有资源(图片、CSS、JS)加载完成
(用户交互)
beforeunload ← 用户即将离开
unload ← 页面销毁

15.2 性能指标(Web Vitals)#

Google 定义了一系列核心 Web 指标

指标全称含义优秀阈值
FPFirst Paint首次绘制-
FCPFirst Contentful Paint首次内容绘制< 1.8s
LCPLargest Contentful Paint最大内容绘制< 2.5s
FIDFirst Input Delay首次输入延迟< 100ms
INPInteraction to Next Paint交互到下一帧< 200ms
CLSCumulative Layout Shift累积布局偏移< 0.1
TTFBTime to First Byte首字节时间< 800ms
TTITime to Interactive可交互时间< 3.8s

15.3 用户感知的”加载完成”#

技术上的 load 事件 ≠ 用户感知的加载完成。比如:

  • 现代 SPA 在 load 事件后还会发起大量 API 请求
  • 图片懒加载(IntersectionObserver)让 load 时机大幅提前
  • 骨架屏让用户感知速度更快

附加知识:被忽略的细节#

A. 浏览器多进程架构#

Chrome 不是一个进程,而是多进程:

进程数量作用
Browser1主进程,UI、网络、存储
Renderer多个每个 Tab 一个,负责渲染
GPU1处理 GPU 任务
Network1网络服务(Chrome 73+独立)
Plugin多个插件进程
Utility多个各种工具进程

站点隔离(Site Isolation):不同源的网站在不同进程,防止 Spectre 类侧信道攻击。

B. 跨域机制#

浏览器的同源策略(Same-Origin Policy):协议 + 域名 + 端口完全一致才同源。

跨域解决方案:

  • CORS:服务器返回 Access-Control-Allow-Origin
  • JSONP:利用 <script> 标签不受同源策略限制(已过时)
  • postMessage:跨窗口通信
  • document.domain:同主域不同子域之间通信(已废弃)
  • 代理:开发环境用 Nginx / webpack devServer

C. 预加载策略#

指令作用
dns-prefetch预解析 DNS
preconnect预建立 TCP/TLS 连接
prefetch预下载未来可能用到的资源
preload预下载当前页面关键资源
prerender预渲染整个页面
modulepreload预加载 ES 模块

D. Service Worker 与 PWA#

Service Worker 是一个独立的 Worker 线程,可以拦截网络请求,实现离线缓存。这让 PWA 可以做到:

  • 离线访问
  • 推送通知
  • 后台同步

E. HTTP/2 与 HTTP/3 的实际差异#

HTTP/2 在 95% 的场景下已经足够,HTTP/3 的优势主要在:

  • 移动网络:经常切换 IP(WiFi → 4G),QUIC 用 Connection ID 而非 IP 标识连接,切换网络不断连
  • 高丢包环境:QUIC 消除队头阻塞
  • 首次连接:QUIC 的 0-RTT 显著降低延迟

F. 浏览器渲染优化技巧#

  1. 避免强制同步布局:不要在循环中读取 offsetWidth 后立即写入样式
  2. 批量 DOM 操作:用 DocumentFragmentrequestAnimationFrame
  3. 使用 CSS Containmentcontain: layout style paint 限制渲染范围
  4. 虚拟列表:长列表只渲染可见部分
  5. 图片优化:WebP/AVIF 格式 + loading="lazy" + srcset 响应式
  6. 代码分割:动态 import + 路由级懒加载
  7. Tree Shaking:移除未使用代码
  8. Critical CSS:内联首屏关键 CSS

G. 安全相关#

完整加载过程中浏览器还会处理一系列安全策略:

  • CSP(Content Security Policy):限制资源来源
  • CORS:跨域资源共享
  • CORP / COEP / COOP:跨源隔离
  • Trusted Types:防 DOM XSS
  • Subresource Integrity(SRI):第三方资源完整性校验
  • Referrer-Policy:控制 Referer 头
  • Permissions-Policy:控制 API 权限

完整时间线示例#

以访问 https://github.com 为例,一次完整加载的时间分布大致:

0ms │ 按下 Enter
1ms │ URL 解析、安全检查
2ms │ 检查缓存(未命中)
3ms │ DNS 查询开始
20ms │ DNS 查询完成
20ms │ TCP 三次握手开始
50ms │ TCP 连接建立
50ms │ TLS 握手开始(TLS 1.3,1 RTT)
80ms │ TLS 握手完成
80ms │ 发送 HTTP 请求
180ms │ 接收到第一个字节(TTFB)
200ms │ HTML 流式开始解析
220ms │ 发现 CSS、JS 资源,开始下载
350ms │ CSS 下载完成,CSSOM 构建完成
400ms │ JS 下载完成,开始执行
500ms │ DOM 解析完成(DOMContentLoaded)
500ms │ First Paint
700ms │ First Contentful Paint
1200ms │ Largest Contentful Paint
1500ms │ 图片全部加载完成
1500ms │ load 事件触发
2000ms │ Time to Interactive

面试回答策略#

实战中,面试官问这道题,你不需要把上面所有内容都背一遍。建议分层次回答

第一层(30 秒):#

“首先 URL 解析和安全检查,然后查询 DNS 拿到 IP,三次握手建立 TCP 连接,HTTPS 还要做 TLS 握手,然后发送 HTTP 请求,服务器返回 HTML,浏览器解析 HTML 构建 DOM、解析 CSS 构建 CSSOM、执行 JS,然后 Layout、Paint、Composite,最后触发 load 事件。“

第二层(2 分钟):#

挑面试官感兴趣的环节展开。常见的深挖方向:

  • DNS:递归查询、DoH
  • TCP:为什么三次握手、TCP Fast Open
  • TLS:1.2 vs 1.3、证书验证
  • HTTP:1.1 vs 2 vs 3、缓存策略
  • 渲染:关键渲染路径、重排重绘
  • JS:事件循环、V8 优化
  • 性能:Web Vitals、优化手段

第三层(10 分钟):#

如果面试官继续追问,可以引出:

  • 浏览器多进程架构
  • 站点隔离
  • HTTP/3 与 QUIC
  • 性能优化案例
  • 安全策略

结语#

这道题之所以经典,是因为它像一根线,把整个 Web 技术栈串了起来

从硬件中断到 V8 优化、从 TCP 握手到 CSS 合成、从 DNS 查询到 Layout 重排——每一个环节都是一个独立的工程领域,每一个细节都可能成为面试官追问的方向。

真正掌握这道题,意味着你已经具备了全栈视角:你不再只是写 React 组件的人,而是理解整个 Web 平台运作原理的工程师。这才是高级前端与初级前端的真正分水岭。


主要参考资料

  • 《图解 HTTP》上野宣
  • 《HTTP 权威指南》David Gourley
  • 《WebKit 技术内幕》朱永盛
  • 《浏览器工作原理与实践》李兵(极客时间)
  • Google Web.dev 官方文档(https://web.dev
  • Chrome Developers(https://developer.chrome.com
  • MDN Web Docs(https://developer.mozilla.org
  • RFC 7230-7235(HTTP/1.1)
  • RFC 7540(HTTP/2)
  • RFC 9000(QUIC)
  • RFC 9114(HTTP/3)
  • RFC 8446(TLS 1.3)
  • V8 官方博客(https://v8.dev
  • 《What happens when》仓库(GitHub 经典开源项目)
从在浏览器输入网址到页面完全加载完成,这中间发生了什么?
https://fuwari.vercel.app/posts/browser-url-to-page-loaded/
Author
Owen
Published at
2026-05-28
License
CC BY-NC-SA 4.0