“从在浏览器输入网址到页面完全加载完成,这中间发生了什么?”
这是前端面试中最经典、也最能拉开候选人差距的一道题。表面看是一个简单的过程描述,但深入挖掘可以涉及操作系统、网络协议、浏览器架构、渲染引擎、JS 引擎、安全机制、性能优化等几乎所有计算机基础知识。
回答这道题的层次差异:
- 初级回答:DNS → TCP → HTTP → 渲染(5 句话答完)
- 中级回答:能讲清楚 TCP 三次握手、HTTPS 加密、关键渲染路径
- 高级回答:能讲清楚 QUIC、HTTP/2、TLS 1.3、浏览器多进程架构、合成线程、Speculative Parsing 等
本文按照高级回答的标准,从你按下键盘那一刻开始,把整个过程拆解成 15 个阶段,每个阶段都尽量写到底。
阶段总览
| 阶段 | 关键动作 | 涉及层级 |
|---|---|---|
| 1 | 键盘输入 | 硬件 + 操作系统 |
| 2 | URL 解析与补全 | 浏览器 |
| 3 | 安全检查与拦截 | 浏览器 |
| 4 | 检查缓存 | 浏览器 |
| 5 | DNS 查询 | 应用层 |
| 6 | 建立 TCP 连接 | 传输层 |
| 7 | TLS 握手(HTTPS) | 表示层 |
| 8 | 发送 HTTP 请求 | 应用层 |
| 9 | 服务器处理与响应 | 后端 |
| 10 | 浏览器接收响应 | 浏览器 |
| 11 | 解析 HTML 构建 DOM | 渲染引擎 |
| 12 | 解析 CSS 构建 CSSOM | 渲染引擎 |
| 13 | 执行 JavaScript | JS 引擎 |
| 14 | 渲染:Layout → Paint → Composite | 渲染引擎 + GPU |
| 15 | 触发各种事件,加载完成 | 浏览器 |
下面逐个展开。
阶段 1:键盘输入到浏览器接收字符
这一步几乎所有教程都跳过,但其实信息量很大。
1.1 硬件信号
当你按下键盘上的 G 键时:
- 键盘内的**键盘控制器(Keyboard Controller)**检测到按键闭合,产生扫描码(Scan Code)
- 通过 USB/PS2/蓝牙协议传输到主板
- 主板**中断控制器(PIC/APIC)**触发硬件中断(IRQ 1)
- CPU 暂停当前任务,跳转到键盘中断处理程序
1.2 操作系统处理
- 内核驱动接收扫描码,转换成键码(Key Code)
- 通过 OS 的输入子系统(如 Linux 的 evdev、Windows 的 RawInput)传递给当前焦点窗口
- 浏览器进程通过事件循环接收到键盘事件
1.3 浏览器处理
- 浏览器的 UI 进程接收到键盘事件
- 判断焦点在地址栏(Omnibox),将字符送入地址栏组件
- 触发自动补全:从历史记录、书签、搜索引擎建议中匹配
- 当用户按下 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 转换:
中文域名: 中国.cnPunycode: 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, publicExpires: Wed, 28 May 2026 12:00:00 GMT协商缓存:浏览器发请求,服务器决定是否使用本地副本。
If-Modified-Since: Wed, 28 May 2026 10:00:00 GMTIf-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 → 权威 DNS5.2 递归查询过程
以查询 www.github.com 为例:
- 客户端 → 本地 DNS 服务器:你知道
www.github.com的 IP 吗? - 本地 DNS → 根 DNS(
.):你知道吗?根 DNS:不知道,去问.com服务器 - 本地 DNS →
.comTLD 服务器:你知道吗?TLD:不知道,去问github.com的权威服务器 - 本地 DNS →
github.com权威服务器:你知道吗?权威:知道,IP 是140.82.114.4 - 本地 DNS → 客户端:IP 是
140.82.114.4
5.3 DNS 记录类型
| 类型 | 含义 |
|---|---|
| A | IPv4 地址 |
| AAAA | IPv6 地址 |
| 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 证书验证
浏览器收到服务器证书后,进行以下验证:
- 域名匹配:证书中的 CN/SAN 字段是否匹配当前域名
- 有效期:是否在
Not Before和Not After之间 - 证书链:从根 CA 到当前证书是否完整
- 吊销状态:通过 CRL(证书吊销列表)或 OCSP(在线证书状态协议)查询
- 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.1Host: github.comUser-Agent: Mozilla/5.0 ...Accept: text/html,application/xhtml+xml,...Accept-Language: zh-CN,zh;q=0.9Accept-Encoding: gzip, deflate, brCookie: _octo=GH1.1.xxx; user_session=xxxConnection: keep-aliveUpgrade-Insecure-Requests: 18.2 HTTP 各版本对比
| 版本 | 年份 | 关键特性 |
|---|---|---|
| HTTP/0.9 | 1991 | 只支持 GET |
| HTTP/1.0 | 1996 | 支持 POST、HEAD、状态码、头部 |
| HTTP/1.1 | 1997 | 长连接(keep-alive)、管线化(pipelining)、Host 头 |
| HTTP/2 | 2015 | 二进制分帧、多路复用、头部压缩(HPACK)、服务端推送 |
| HTTP/3 | 2022 | 基于 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。
8.5 Cookie 与同源策略
浏览器在发请求时会自动附带 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 等)的处理流程:
- 路由匹配
- 中间件链(鉴权、日志、参数解析)
- 业务逻辑(查询数据库、调用其他服务)
- 模板渲染(如果是 SSR)
- 构造响应
9.3 数据库查询
应用服务器 → 数据库连接池 → 数据库引擎 → 缓存(Redis)→ 磁盘很多前端面试题会延伸问到:
- N+1 查询问题:循环中查询数据库导致大量请求
- 索引:B+ 树、Hash 索引的差异
- 事务隔离级别:读未提交、读已提交、可重复读、串行化
9.4 响应格式
HTTP/1.1 200 OKDate: Wed, 28 May 2026 12:00:00 GMTServer: GitHub.comContent-Type: text/html; charset=utf-8Content-Length: 142857Cache-Control: max-age=0, privateSet-Cookie: _gh_sess=xxx; Path=/; Secure; HttpOnly; SameSite=LaxStrict-Transport-Security: max-age=31536000X-Content-Type-Options: nosniffX-Frame-Options: denyContent-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 网关超时 |
附加知识:
301和302都是重定向,但 SEO 含义不同。301表示永久重定向,搜索引擎会更新索引;302表示临时重定向,搜索引擎保留原 URL。
阶段 10:浏览器接收响应
浏览器接收响应是一个流式过程——不是等全部数据到达才开始处理,而是边接收边解析。
10.1 内容协商
根据 Content-Type 决定如何处理:
text/html→ HTML 解析器application/json→ JSON 数据image/*→ 图像解码器application/octet-stream→ 下载
10.2 内容解压
如果 Content-Encoding 是 gzip 或 br(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: fixedtransform: translateZ(0)或will-change: transform- 3D 变换
- video、canvas、iframe
- 滤镜(filter)
只触发 Composite 不触发 Layout/Paint 的属性:
- transform
- opacity
这就是为什么动画推荐用 transform 和 opacity——它们只走合成线程,不走主线程,性能极高。
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 指标:
| 指标 | 全称 | 含义 | 优秀阈值 |
|---|---|---|---|
| FP | First Paint | 首次绘制 | - |
| FCP | First Contentful Paint | 首次内容绘制 | < 1.8s |
| LCP | Largest Contentful Paint | 最大内容绘制 | < 2.5s |
| FID | First Input Delay | 首次输入延迟 | < 100ms |
| INP | Interaction to Next Paint | 交互到下一帧 | < 200ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | < 0.1 |
| TTFB | Time to First Byte | 首字节时间 | < 800ms |
| TTI | Time to Interactive | 可交互时间 | < 3.8s |
15.3 用户感知的”加载完成”
技术上的 load 事件 ≠ 用户感知的加载完成。比如:
- 现代 SPA 在
load事件后还会发起大量 API 请求 - 图片懒加载(IntersectionObserver)让
load时机大幅提前 - 骨架屏让用户感知速度更快
附加知识:被忽略的细节
A. 浏览器多进程架构
Chrome 不是一个进程,而是多进程:
| 进程 | 数量 | 作用 |
|---|---|---|
| Browser | 1 | 主进程,UI、网络、存储 |
| Renderer | 多个 | 每个 Tab 一个,负责渲染 |
| GPU | 1 | 处理 GPU 任务 |
| Network | 1 | 网络服务(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. 浏览器渲染优化技巧
- 避免强制同步布局:不要在循环中读取
offsetWidth后立即写入样式 - 批量 DOM 操作:用
DocumentFragment或requestAnimationFrame - 使用 CSS Containment:
contain: layout style paint限制渲染范围 - 虚拟列表:长列表只渲染可见部分
- 图片优化:WebP/AVIF 格式 +
loading="lazy"+srcset响应式 - 代码分割:动态 import + 路由级懒加载
- Tree Shaking:移除未使用代码
- 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 │ 按下 Enter1ms │ 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 Paint700ms │ First Contentful Paint1200ms │ Largest Contentful Paint1500ms │ 图片全部加载完成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 经典开源项目)