浏览器看起来只是一个"打开网页"的工具,内部却同时包含网络协议栈、JavaScript 引擎、渲染引擎、进程管理、缓存系统、存储系统、安全沙箱和开发者工具。
本文将从用户在地址栏输入 URL 开始,完整拆解网页经历的网络请求、HTML 解析、CSS 计算、JavaScript 执行、页面渲染和像素合成过程,并进一步介绍性能优化、安全策略、故障排查与工程实践。
1. 浏览器到底是什么
从普通用户的角度看,浏览器是一个访问网站的应用程序。
从开发者的角度看,浏览器可以理解成一个运行 Web 应用的操作系统:
- 通过 HTTP、HTTPS、WebSocket 等协议访问网络。
- 解析 HTML,构建页面结构。
- 解析 CSS,计算页面样式。
- 执行 JavaScript。
- 将页面绘制成屏幕上的像素。
- 管理 Cookie、缓存、IndexedDB 等数据。
- 提供摄像头、定位、通知、剪贴板等系统能力。
- 通过同源策略、沙箱和站点隔离保护用户数据。
- 为多个标签页、扩展程序和后台任务分配进程与资源。
一个现代浏览器至少包含以下核心模块:
| 模块 | 主要职责 |
|---|---|
| 浏览器界面 | 地址栏、前进、后退、书签、标签页 |
| 浏览器进程 | 管理窗口、标签页、权限、导航和其他进程 |
| 网络服务 | DNS、连接、TLS、HTTP、代理、缓存 |
| 渲染进程 | 解析 HTML/CSS、执行 JavaScript、计算布局 |
| 渲染引擎 | 将 HTML 和 CSS 转换为可绘制内容 |
| JavaScript 引擎 | 解析、编译和执行 JavaScript |
| GPU 进程 | 栅格化、纹理处理和图层合成 |
| 存储服务 | Cookie、LocalStorage、IndexedDB、Cache API |
| 扩展进程 | 隔离运行浏览器扩展 |
| 工具进程 | 音视频解码、PDF、数据解析等辅助任务 |
不同浏览器使用的引擎并不完全相同:
| 浏览器 | 渲染引擎 | JavaScript 引擎 |
|---|---|---|
| Chrome、Edge、Opera | Blink | V8 |
| Firefox | Gecko | SpiderMonkey |
| Safari | WebKit | JavaScriptCore |
本文会使用 Chromium 的架构解释具体过程,但大部分 Web 标准和工程原则同样适用于 Firefox、Safari 等浏览器。
2. 浏览器为什么采用多进程架构
早期浏览器可能将界面、网络、页面渲染和插件都放在同一个进程中。
这种方式虽然简单,却存在严重问题:
- 一个网页崩溃可能导致整个浏览器退出。
- 一个标签页死循环可能卡住所有标签页。
- 恶意网页更容易访问浏览器内部数据。
- 很难充分利用多核 CPU。
- 插件崩溃会影响整个浏览器。
现代浏览器普遍使用多进程架构。
以 Chromium 为例,其结构可以概括为:
#mermaid-svg-UCK2TuZspm4kQbVe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UCK2TuZspm4kQbVe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UCK2TuZspm4kQbVe .error-icon{fill:#552222;}#mermaid-svg-UCK2TuZspm4kQbVe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UCK2TuZspm4kQbVe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UCK2TuZspm4kQbVe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UCK2TuZspm4kQbVe .marker.cross{stroke:#333333;}#mermaid-svg-UCK2TuZspm4kQbVe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UCK2TuZspm4kQbVe p{margin:0;}#mermaid-svg-UCK2TuZspm4kQbVe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UCK2TuZspm4kQbVe .cluster-label text{fill:#333;}#mermaid-svg-UCK2TuZspm4kQbVe .cluster-label span{color:#333;}#mermaid-svg-UCK2TuZspm4kQbVe .cluster-label span p{background-color:transparent;}#mermaid-svg-UCK2TuZspm4kQbVe .label text,#mermaid-svg-UCK2TuZspm4kQbVe span{fill:#333;color:#333;}#mermaid-svg-UCK2TuZspm4kQbVe .node rect,#mermaid-svg-UCK2TuZspm4kQbVe .node circle,#mermaid-svg-UCK2TuZspm4kQbVe .node ellipse,#mermaid-svg-UCK2TuZspm4kQbVe .node polygon,#mermaid-svg-UCK2TuZspm4kQbVe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UCK2TuZspm4kQbVe .rough-node .label text,#mermaid-svg-UCK2TuZspm4kQbVe .node .label text,#mermaid-svg-UCK2TuZspm4kQbVe .image-shape .label,#mermaid-svg-UCK2TuZspm4kQbVe .icon-shape .label{text-anchor:middle;}#mermaid-svg-UCK2TuZspm4kQbVe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UCK2TuZspm4kQbVe .rough-node .label,#mermaid-svg-UCK2TuZspm4kQbVe .node .label,#mermaid-svg-UCK2TuZspm4kQbVe .image-shape .label,#mermaid-svg-UCK2TuZspm4kQbVe .icon-shape .label{text-align:center;}#mermaid-svg-UCK2TuZspm4kQbVe .node.clickable{cursor:pointer;}#mermaid-svg-UCK2TuZspm4kQbVe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UCK2TuZspm4kQbVe .arrowheadPath{fill:#333333;}#mermaid-svg-UCK2TuZspm4kQbVe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UCK2TuZspm4kQbVe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UCK2TuZspm4kQbVe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UCK2TuZspm4kQbVe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UCK2TuZspm4kQbVe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UCK2TuZspm4kQbVe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UCK2TuZspm4kQbVe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UCK2TuZspm4kQbVe .cluster text{fill:#333;}#mermaid-svg-UCK2TuZspm4kQbVe .cluster span{color:#333;}#mermaid-svg-UCK2TuZspm4kQbVe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UCK2TuZspm4kQbVe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UCK2TuZspm4kQbVe rect.text{fill:none;stroke-width:0;}#mermaid-svg-UCK2TuZspm4kQbVe .icon-shape,#mermaid-svg-UCK2TuZspm4kQbVe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UCK2TuZspm4kQbVe .icon-shape p,#mermaid-svg-UCK2TuZspm4kQbVe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UCK2TuZspm4kQbVe .icon-shape .label rect,#mermaid-svg-UCK2TuZspm4kQbVe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UCK2TuZspm4kQbVe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UCK2TuZspm4kQbVe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UCK2TuZspm4kQbVe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 浏览器界面
Browser Process
浏览器进程
Network Service
网络服务
Renderer Process A
渲染进程
Renderer Process B
渲染进程
GPU Process
GPU 进程
Storage Service
存储服务
Utility Process
工具进程
Extension Process
扩展进程
2.1 浏览器进程
浏览器进程负责:
- 地址栏和标签页管理。
- 前进、后退、刷新。
- 导航决策。
- 权限申请。
- 下载管理。
- 进程创建和回收。
- 与操作系统交互。
- 协调网络进程、渲染进程和 GPU 进程。
它相当于整个浏览器的总控制中心。
2.2 渲染进程
渲染进程负责处理网页内容,包括:
- HTML 解析。
- CSS 解析。
- DOM 和样式计算。
- 页面布局。
- JavaScript 执行。
- 生成绘制指令。
- 处理大部分页面事件。
渲染进程通常运行在权限受限的沙箱中,不能随意访问本地文件和操作系统资源。
2.3 GPU 进程
GPU 进程主要负责:
- 页面图层合成。
- 栅格化任务。
- CSS 变换。
- 部分动画。
- Canvas 和 WebGL。
- 视频渲染。
需要注意,GPU 并不是负责网页渲染的全部过程。DOM、样式计算和大部分布局工作通常仍然发生在渲染进程的主线程中。
2.4 网络服务
网络服务负责:
- DNS 解析。
- TCP、QUIC 连接。
- TLS 握手。
- HTTP/1.1、HTTP/2、HTTP/3。
- 代理配置。
- Cookie 附加。
- HTTP 缓存。
- 证书校验。
- 网络安全策略。
2.5 一个标签页是否对应一个进程
"一个标签页对应一个渲染进程"只是简化说法,并不准确。
实际情况更加复杂:
- 一个标签页可能包含多个跨站 iframe。
- 跨站 iframe 可能运行在不同渲染进程中。
- 同站点页面可能复用渲染进程。
- 浏览器可能因内存限制调整进程分配。
- 标签页导航到另一个站点时可能切换渲染进程。
- 浏览器内部页面和扩展页面可能使用专门的进程。
因此,更准确的理解是:
浏览器根据站点隔离、安全策略、页面关系和系统资源动态分配渲染进程。
3. 从输入 URL 到页面显示的完整流程
假设用户在地址栏输入:
text
https://www.example.com/products?id=100#detail
浏览器需要完成以下主要阶段:
#mermaid-svg-Lf4WEbyi2nA3gBNc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Lf4WEbyi2nA3gBNc .error-icon{fill:#552222;}#mermaid-svg-Lf4WEbyi2nA3gBNc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Lf4WEbyi2nA3gBNc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .marker.cross{stroke:#333333;}#mermaid-svg-Lf4WEbyi2nA3gBNc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Lf4WEbyi2nA3gBNc p{margin:0;}#mermaid-svg-Lf4WEbyi2nA3gBNc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .cluster-label text{fill:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .cluster-label span{color:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .cluster-label span p{background-color:transparent;}#mermaid-svg-Lf4WEbyi2nA3gBNc .label text,#mermaid-svg-Lf4WEbyi2nA3gBNc span{fill:#333;color:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .node rect,#mermaid-svg-Lf4WEbyi2nA3gBNc .node circle,#mermaid-svg-Lf4WEbyi2nA3gBNc .node ellipse,#mermaid-svg-Lf4WEbyi2nA3gBNc .node polygon,#mermaid-svg-Lf4WEbyi2nA3gBNc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .rough-node .label text,#mermaid-svg-Lf4WEbyi2nA3gBNc .node .label text,#mermaid-svg-Lf4WEbyi2nA3gBNc .image-shape .label,#mermaid-svg-Lf4WEbyi2nA3gBNc .icon-shape .label{text-anchor:middle;}#mermaid-svg-Lf4WEbyi2nA3gBNc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .rough-node .label,#mermaid-svg-Lf4WEbyi2nA3gBNc .node .label,#mermaid-svg-Lf4WEbyi2nA3gBNc .image-shape .label,#mermaid-svg-Lf4WEbyi2nA3gBNc .icon-shape .label{text-align:center;}#mermaid-svg-Lf4WEbyi2nA3gBNc .node.clickable{cursor:pointer;}#mermaid-svg-Lf4WEbyi2nA3gBNc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .arrowheadPath{fill:#333333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Lf4WEbyi2nA3gBNc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Lf4WEbyi2nA3gBNc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Lf4WEbyi2nA3gBNc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Lf4WEbyi2nA3gBNc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .cluster text{fill:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc .cluster span{color:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Lf4WEbyi2nA3gBNc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Lf4WEbyi2nA3gBNc rect.text{fill:none;stroke-width:0;}#mermaid-svg-Lf4WEbyi2nA3gBNc .icon-shape,#mermaid-svg-Lf4WEbyi2nA3gBNc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Lf4WEbyi2nA3gBNc .icon-shape p,#mermaid-svg-Lf4WEbyi2nA3gBNc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Lf4WEbyi2nA3gBNc .icon-shape .label rect,#mermaid-svg-Lf4WEbyi2nA3gBNc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Lf4WEbyi2nA3gBNc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Lf4WEbyi2nA3gBNc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Lf4WEbyi2nA3gBNc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户输入 URL
解析 URL
检查导航策略和缓存
查找 Service Worker
能否直接返回响应
接收 HTTP 响应
DNS 解析
建立 TCP 或 QUIC 连接
TLS 握手
发送 HTTP 请求
服务器处理请求
解析 HTML
加载 CSS、JS、图片、字体
构建 DOM 和样式数据
样式计算
布局 Layout
绘制 Paint
栅格化 Raster
图层合成 Composite
屏幕显示页面
这张图是概念上的主流程。实际浏览器会并行执行很多任务,例如:
- HTML 还没下载完成时就开始解析。
- 解析 HTML 时并行下载 CSS、JavaScript 和图片。
- 浏览器可能复用已经建立的网络连接。
- DNS 查询、预连接和缓存检查可能提前发生。
- 页面局部内容可能先绘制,其他内容后显示。
- Service Worker 可能直接从 Cache API 返回响应。
- 前进或后退时可能直接恢复内存中的页面。
4. URL 解析与导航
一个标准 URL 通常包含以下组成部分:
text
https://user:password@www.example.com:443/products?id=100#detail
|___| |_____________| |_______________| |_______| |______|
协议 用户信息 主机和端口 路径 查询与片段
更常见的形式是:
text
https://www.example.com:443/products?id=100#detail
各部分含义如下:
| 部分 | 示例 | 说明 |
|---|---|---|
| Scheme | https |
使用的协议 |
| Host | www.example.com |
目标主机 |
| Port | 443 |
目标端口 |
| Path | /products |
资源路径 |
| Query | id=100 |
查询参数 |
| Fragment | detail |
页面内片段标识 |
需要注意:
#detail通常不会发送给服务器。- Fragment 主要由浏览器在客户端处理。
- Query 参数会作为请求目标的一部分发送给服务器。
- HTTPS 默认端口是 443。
- HTTP 默认端口是 80。
- 国际化域名可能在底层转换为 Punycode。
- 地址栏内容不一定被当作 URL,也可能被识别为搜索关键词。
4.1 浏览器开始导航前会做什么
浏览器进程通常会进行以下检查:
- 判断输入内容是 URL 还是搜索词。
- 对 URL 进行标准化。
- 检查是否需要从 HTTP 升级到 HTTPS。
- 检查企业代理、安全策略和扩展拦截规则。
- 判断是否为页面内 Fragment 导航。
- 判断是否为浏览器内部页面。
- 检查当前页面是否注册了离开确认。
- 选择或创建合适的渲染进程。
- 创建导航请求。
- 将请求交给网络服务或 Service Worker。
5. DNS:浏览器如何找到服务器
浏览器不能直接通过域名连接服务器,必须先得到服务器的 IP 地址。
例如:
text
www.example.com
可能被解析为:
text
93.184.216.34
DNS 查询大致会经过:
#mermaid-svg-zf2RDntZ8QZLwNvE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zf2RDntZ8QZLwNvE .error-icon{fill:#552222;}#mermaid-svg-zf2RDntZ8QZLwNvE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zf2RDntZ8QZLwNvE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zf2RDntZ8QZLwNvE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zf2RDntZ8QZLwNvE .marker.cross{stroke:#333333;}#mermaid-svg-zf2RDntZ8QZLwNvE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zf2RDntZ8QZLwNvE p{margin:0;}#mermaid-svg-zf2RDntZ8QZLwNvE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE .cluster-label text{fill:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE .cluster-label span{color:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE .cluster-label span p{background-color:transparent;}#mermaid-svg-zf2RDntZ8QZLwNvE .label text,#mermaid-svg-zf2RDntZ8QZLwNvE span{fill:#333;color:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE .node rect,#mermaid-svg-zf2RDntZ8QZLwNvE .node circle,#mermaid-svg-zf2RDntZ8QZLwNvE .node ellipse,#mermaid-svg-zf2RDntZ8QZLwNvE .node polygon,#mermaid-svg-zf2RDntZ8QZLwNvE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zf2RDntZ8QZLwNvE .rough-node .label text,#mermaid-svg-zf2RDntZ8QZLwNvE .node .label text,#mermaid-svg-zf2RDntZ8QZLwNvE .image-shape .label,#mermaid-svg-zf2RDntZ8QZLwNvE .icon-shape .label{text-anchor:middle;}#mermaid-svg-zf2RDntZ8QZLwNvE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zf2RDntZ8QZLwNvE .rough-node .label,#mermaid-svg-zf2RDntZ8QZLwNvE .node .label,#mermaid-svg-zf2RDntZ8QZLwNvE .image-shape .label,#mermaid-svg-zf2RDntZ8QZLwNvE .icon-shape .label{text-align:center;}#mermaid-svg-zf2RDntZ8QZLwNvE .node.clickable{cursor:pointer;}#mermaid-svg-zf2RDntZ8QZLwNvE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zf2RDntZ8QZLwNvE .arrowheadPath{fill:#333333;}#mermaid-svg-zf2RDntZ8QZLwNvE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zf2RDntZ8QZLwNvE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zf2RDntZ8QZLwNvE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zf2RDntZ8QZLwNvE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zf2RDntZ8QZLwNvE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zf2RDntZ8QZLwNvE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zf2RDntZ8QZLwNvE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zf2RDntZ8QZLwNvE .cluster text{fill:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE .cluster span{color:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zf2RDntZ8QZLwNvE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zf2RDntZ8QZLwNvE rect.text{fill:none;stroke-width:0;}#mermaid-svg-zf2RDntZ8QZLwNvE .icon-shape,#mermaid-svg-zf2RDntZ8QZLwNvE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zf2RDntZ8QZLwNvE .icon-shape p,#mermaid-svg-zf2RDntZ8QZLwNvE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zf2RDntZ8QZLwNvE .icon-shape .label rect,#mermaid-svg-zf2RDntZ8QZLwNvE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zf2RDntZ8QZLwNvE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zf2RDntZ8QZLwNvE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zf2RDntZ8QZLwNvE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 浏览器 DNS 缓存
操作系统 DNS 缓存
Hosts 文件
本地 DNS 解析器
递归 DNS 服务器
根域名服务器
顶级域服务器
权威 DNS 服务器
返回 IP 地址
实际查询不一定经过所有步骤,因为缓存可能直接命中。
5.1 DNS 缓存
DNS 结果可能存在于:
- 浏览器 DNS 缓存。
- 操作系统 DNS 缓存。
- 路由器缓存。
- 运营商或公共 DNS 缓存。
- 本地 Hosts 文件。
- 企业内部 DNS。
- 权威 DNS 服务器。
DNS 记录通过 TTL 控制缓存时间。
5.2 常见 DNS 记录
| 记录 | 作用 |
|---|---|
| A | 域名映射到 IPv4 地址 |
| AAAA | 域名映射到 IPv6 地址 |
| CNAME | 域名映射到另一个域名 |
| NS | 指定权威 DNS 服务器 |
| TXT | 保存文本信息 |
| MX | 指定邮件服务器 |
| HTTPS | 描述 HTTPS 服务和连接提示 |
5.3 DNS 优化
可以通过 dns-prefetch 提前解析后续可能访问的域名:
html
<link rel="dns-prefetch" href="//static.example.com">
它只进行 DNS 解析,不建立连接。
如果确定某个跨域资源马上会被使用,可以考虑:
html
<link rel="preconnect" href="https://static.example.com" crossorigin>
preconnect 可能提前完成:
- DNS 解析。
- TCP 或 QUIC 连接准备。
- TLS 握手。
不要为大量域名添加 preconnect,否则会增加连接、CPU 和电量开销。
6. 建立网络连接
得到 IP 地址后,浏览器需要与服务器建立连接。
浏览器实际使用的连接方式与 HTTP 版本有关。
6.1 HTTP/1.1
HTTP/1.1 通常运行在 TCP 之上。
建立 TCP 连接需要三次握手:
服务器 浏览器 服务器 浏览器 #mermaid-svg-d4DO0tNhgKs8q1Q5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .error-icon{fill:#552222;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .marker.cross{stroke:#333333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-d4DO0tNhgKs8q1Q5 p{margin:0;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-d4DO0tNhgKs8q1Q5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-d4DO0tNhgKs8q1Q5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .sequenceNumber{fill:white;}#mermaid-svg-d4DO0tNhgKs8q1Q5 #sequencenumber{fill:#333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .messageText{fill:#333;stroke:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .labelText,#mermaid-svg-d4DO0tNhgKs8q1Q5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .loopText,#mermaid-svg-d4DO0tNhgKs8q1Q5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-d4DO0tNhgKs8q1Q5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .noteText,#mermaid-svg-d4DO0tNhgKs8q1Q5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .actorPopupMenu{position:absolute;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-d4DO0tNhgKs8q1Q5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-d4DO0tNhgKs8q1Q5 .actor-man circle,#mermaid-svg-d4DO0tNhgKs8q1Q5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-d4DO0tNhgKs8q1Q5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TCP 连接建立 SYN SYN + ACK ACK
HTTP/1.1 本身没有多路复用能力。浏览器通常会对同一来源建立多个 TCP 连接,以并行下载资源。
6.2 HTTPS 与 TLS
HTTPS 可以理解为:
text
HTTPS = HTTP + TLS
TLS 主要提供:
- 服务器身份验证。
- 数据加密。
- 数据完整性保护。
- 可选的客户端证书认证。
简化后的 TLS 过程如下:
服务器 浏览器 服务器 浏览器 #mermaid-svg-yOLzt8NyubmYxYfP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yOLzt8NyubmYxYfP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yOLzt8NyubmYxYfP .error-icon{fill:#552222;}#mermaid-svg-yOLzt8NyubmYxYfP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yOLzt8NyubmYxYfP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yOLzt8NyubmYxYfP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yOLzt8NyubmYxYfP .marker.cross{stroke:#333333;}#mermaid-svg-yOLzt8NyubmYxYfP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yOLzt8NyubmYxYfP p{margin:0;}#mermaid-svg-yOLzt8NyubmYxYfP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yOLzt8NyubmYxYfP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-yOLzt8NyubmYxYfP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yOLzt8NyubmYxYfP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-yOLzt8NyubmYxYfP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-yOLzt8NyubmYxYfP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-yOLzt8NyubmYxYfP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-yOLzt8NyubmYxYfP .sequenceNumber{fill:white;}#mermaid-svg-yOLzt8NyubmYxYfP #sequencenumber{fill:#333;}#mermaid-svg-yOLzt8NyubmYxYfP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-yOLzt8NyubmYxYfP .messageText{fill:#333;stroke:none;}#mermaid-svg-yOLzt8NyubmYxYfP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yOLzt8NyubmYxYfP .labelText,#mermaid-svg-yOLzt8NyubmYxYfP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-yOLzt8NyubmYxYfP .loopText,#mermaid-svg-yOLzt8NyubmYxYfP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-yOLzt8NyubmYxYfP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yOLzt8NyubmYxYfP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yOLzt8NyubmYxYfP .noteText,#mermaid-svg-yOLzt8NyubmYxYfP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-yOLzt8NyubmYxYfP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yOLzt8NyubmYxYfP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yOLzt8NyubmYxYfP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yOLzt8NyubmYxYfP .actorPopupMenu{position:absolute;}#mermaid-svg-yOLzt8NyubmYxYfP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-yOLzt8NyubmYxYfP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yOLzt8NyubmYxYfP .actor-man circle,#mermaid-svg-yOLzt8NyubmYxYfP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-yOLzt8NyubmYxYfP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 支持的 TLS 版本、密码套件、SNI、ALPN 选择参数并返回证书 后续应用数据加密传输 ClientHello ServerHello + Certificate 验证证书链和域名 密钥协商相关消息 完成握手
浏览器验证证书时通常会检查:
- 证书是否过期。
- 域名是否匹配。
- 是否由受信任的证书机构签发。
- 证书链是否完整。
- 证书是否被吊销或存在安全问题。
- 使用的加密参数是否符合安全要求。
6.3 HTTP/2
HTTP/2 主要改进包括:
- 二进制分帧。
- 多路复用。
- 请求和响应流。
- 头部压缩。
- 流优先级相关机制。
- 单连接承载多个并发请求。
HTTP/2 可以在一个 TCP 连接上同时传输多个请求和响应。
但由于底层仍然是 TCP,当 TCP 层发生丢包时,其他 HTTP/2 流也可能等待丢失数据恢复,这属于传输层队头阻塞。
6.4 HTTP/3
HTTP/3 运行在 QUIC 之上,而 QUIC 通常基于 UDP。
它具有以下特点:
- TLS 1.3 集成在 QUIC 握手过程中。
- 支持多路复用。
- 不同流具有相对独立的可靠传输。
- 一个流丢包通常不会直接阻塞其他流的数据交付。
- 支持连接迁移。
- 在部分场景中减少连接建立延迟。
HTTP/3 并不是"完全没有队头阻塞",而是避免了 TCP 连接级别的跨流队头阻塞。单个流内部仍然需要可靠、有序地交付数据。
6.5 HTTP 版本对比
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 常见底层协议 | TCP | TCP | QUIC/UDP |
| 多路复用 | 不支持 | 支持 | 支持 |
| 头部压缩 | 无专用机制 | HPACK | QPACK |
| 跨流队头阻塞 | 容易出现 | TCP 层存在 | 明显改善 |
| TLS | 可选 | 浏览器中通常使用 | 集成 TLS 1.3 |
| 连接迁移 | 不支持 | 不支持 | 支持 |
| 典型优势 | 简单、兼容性好 | 并发效率高 | 弱网和移动网络表现更好 |
浏览器不一定每次都从 HTTP/1.1 逐步升级到 HTTP/3。协议选择可能受到以下因素影响:
- ALPN 协商结果。
- Alt-Svc 响应头。
- DNS HTTPS 记录。
- 浏览器历史连接信息。
- UDP 是否被网络设备阻断。
- 服务器是否支持 HTTP/3。
- 浏览器内部连接竞速策略。
7. HTTP 请求是如何发送的
浏览器建立连接后,会发送 HTTP 请求。
一个概念上的 HTTP/1.1 请求如下:
http
GET /products?id=100 HTTP/1.1
Host: www.example.com
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: session_id=abc123
User-Agent: Mozilla/5.0 ...
请求通常包含:
- 请求方法。
- 请求目标。
- 请求头。
- 可选请求体。
常见请求方法:
| 方法 | 典型用途 |
|---|---|
| GET | 获取资源 |
| POST | 创建资源或提交数据 |
| PUT | 整体更新资源 |
| PATCH | 部分更新资源 |
| DELETE | 删除资源 |
| HEAD | 只获取响应头 |
| OPTIONS | 查询服务器支持能力或进行 CORS 预检 |
浏览器还可能自动添加:
CookieOriginRefererSec-Fetch-SiteSec-Fetch-ModeSec-Fetch-DestAccept-Encoding- 条件缓存请求头
具体请求头会受到浏览器版本、隐私策略、请求类型和站点配置影响。
8. 服务器响应与内容识别
一个典型响应如下:
http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Encoding: br
Cache-Control: no-cache
ETag: "page-v17"
Content-Security-Policy: default-src 'self'
<!doctype html>
<html>
...
</html>
浏览器收到响应后会检查:
- HTTP 状态码。
- Content-Type。
- Content-Encoding。
- 缓存策略。
- 下载文件还是显示页面。
- 重定向位置。
- 安全响应头。
- 内容是否被同源、CORS、CSP 或 MIME 类型策略阻止。
8.1 常见状态码
| 状态码 | 含义 |
|---|---|
| 200 | 请求成功 |
| 204 | 成功但没有响应体 |
| 301 | 永久重定向 |
| 302 | 临时重定向 |
| 304 | 缓存仍然有效 |
| 400 | 请求格式错误 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 429 | 请求过多 |
| 500 | 服务器内部错误 |
| 502 | 网关收到无效响应 |
| 503 | 服务暂时不可用 |
| 504 | 网关等待上游超时 |
8.2 重定向
如果服务器返回:
http
HTTP/1.1 302 Found
Location: https://www.example.com/login
浏览器会根据重定向策略创建新的请求。
重定向会增加额外耗时,因此应避免无意义的多级跳转:
text
http://example.com
-> https://example.com
-> https://www.example.com
-> https://www.example.com/home
更理想的方式是尽量一次跳转到最终地址。
9. 浏览器缓存工作原理
浏览器缓存不是单一缓存,而是多个层次的组合。
| 缓存类型 | 作用 |
|---|---|
| 内存缓存 | 当前浏览器会话中的高速缓存 |
| HTTP 磁盘缓存 | 根据 HTTP 缓存规则保存响应 |
| Service Worker Cache API | 由应用程序主动控制的缓存 |
| BFCache | 保存完整页面状态,用于前进和后退 |
| 图片解码缓存 | 保存已解码的图片数据 |
| DNS 缓存 | 保存域名解析结果 |
| 连接池 | 复用 TCP、TLS 或 QUIC 连接 |
这些缓存的用途不同,不能混为一谈。
9.1 强缓存
服务器可以返回:
http
Cache-Control: public, max-age=31536000, immutable
含义:
public:允许共享缓存保存。max-age=31536000:响应在一年内保持新鲜。immutable:资源在有效期内不会改变。
这种策略适合带内容哈希的静态资源:
text
app.8f32ac1.js
styles.27bd921.css
logo.631e28a.webp
资源内容发生变化时,文件名也发生变化。
9.2 协商缓存
HTML 文件通常不能缓存一年,因为部署后用户需要及时得到新版本。
可以使用:
http
Cache-Control: no-cache
ETag: "index-v42"
no-cache 并不是"不允许缓存",而是表示再次使用前需要向服务器验证。
浏览器可能发送:
http
If-None-Match: "index-v42"
如果内容没有变化,服务器返回:
http
HTTP/1.1 304 Not Modified
浏览器继续使用本地缓存的响应体。
9.3 no-cache 与 no-store
两者经常被混淆:
| 指令 | 含义 |
|---|---|
no-cache |
可以存储,但再次使用前必须验证 |
no-store |
不应存储请求或响应内容 |
max-age=0 |
响应立即过期,通常需要重新验证 |
must-revalidate |
过期后必须向源站验证 |
immutable |
新鲜期内不需要重新验证 |
敏感数据可以考虑:
http
Cache-Control: no-store
普通 HTML 页面更常见的是:
http
Cache-Control: no-cache
9.4 BFCache
BFCache 即 Back/Forward Cache。
当用户离开页面后,浏览器可能将完整页面保存在内存中,包括:
- DOM。
- JavaScript 堆。
- 页面滚动位置。
- 表单状态。
- 事件监听器。
用户点击后退按钮时,浏览器可以直接恢复页面,而不是重新请求和重新渲染。
可以监听页面恢复:
javascript
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
console.log("页面从 BFCache 恢复");
}
});
不要把 BFCache 当成 HTTP 缓存。它们保存的内容和使用场景完全不同。
10. HTML 解析与 DOM 构建
浏览器收到 HTML 字节流后,会进行:
- 字节解码。
- 字符流处理。
- 词法分析。
- Token 生成。
- 树构建。
- DOM 节点创建。
例如:
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>浏览器原理</title>
</head>
<body>
<h1>浏览器工作原理</h1>
<p>从输入 URL 到页面显示。</p>
</body>
</html>
会生成类似的 DOM 树:
#mermaid-svg-AIEtb7zEIWA0NWTj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AIEtb7zEIWA0NWTj .error-icon{fill:#552222;}#mermaid-svg-AIEtb7zEIWA0NWTj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AIEtb7zEIWA0NWTj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AIEtb7zEIWA0NWTj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AIEtb7zEIWA0NWTj .marker.cross{stroke:#333333;}#mermaid-svg-AIEtb7zEIWA0NWTj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AIEtb7zEIWA0NWTj p{margin:0;}#mermaid-svg-AIEtb7zEIWA0NWTj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj .cluster-label text{fill:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj .cluster-label span{color:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj .cluster-label span p{background-color:transparent;}#mermaid-svg-AIEtb7zEIWA0NWTj .label text,#mermaid-svg-AIEtb7zEIWA0NWTj span{fill:#333;color:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj .node rect,#mermaid-svg-AIEtb7zEIWA0NWTj .node circle,#mermaid-svg-AIEtb7zEIWA0NWTj .node ellipse,#mermaid-svg-AIEtb7zEIWA0NWTj .node polygon,#mermaid-svg-AIEtb7zEIWA0NWTj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AIEtb7zEIWA0NWTj .rough-node .label text,#mermaid-svg-AIEtb7zEIWA0NWTj .node .label text,#mermaid-svg-AIEtb7zEIWA0NWTj .image-shape .label,#mermaid-svg-AIEtb7zEIWA0NWTj .icon-shape .label{text-anchor:middle;}#mermaid-svg-AIEtb7zEIWA0NWTj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AIEtb7zEIWA0NWTj .rough-node .label,#mermaid-svg-AIEtb7zEIWA0NWTj .node .label,#mermaid-svg-AIEtb7zEIWA0NWTj .image-shape .label,#mermaid-svg-AIEtb7zEIWA0NWTj .icon-shape .label{text-align:center;}#mermaid-svg-AIEtb7zEIWA0NWTj .node.clickable{cursor:pointer;}#mermaid-svg-AIEtb7zEIWA0NWTj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AIEtb7zEIWA0NWTj .arrowheadPath{fill:#333333;}#mermaid-svg-AIEtb7zEIWA0NWTj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AIEtb7zEIWA0NWTj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AIEtb7zEIWA0NWTj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AIEtb7zEIWA0NWTj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AIEtb7zEIWA0NWTj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AIEtb7zEIWA0NWTj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AIEtb7zEIWA0NWTj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AIEtb7zEIWA0NWTj .cluster text{fill:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj .cluster span{color:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AIEtb7zEIWA0NWTj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AIEtb7zEIWA0NWTj rect.text{fill:none;stroke-width:0;}#mermaid-svg-AIEtb7zEIWA0NWTj .icon-shape,#mermaid-svg-AIEtb7zEIWA0NWTj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AIEtb7zEIWA0NWTj .icon-shape p,#mermaid-svg-AIEtb7zEIWA0NWTj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AIEtb7zEIWA0NWTj .icon-shape .label rect,#mermaid-svg-AIEtb7zEIWA0NWTj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AIEtb7zEIWA0NWTj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AIEtb7zEIWA0NWTj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AIEtb7zEIWA0NWTj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Document
html
head
body
meta
title
h1
p
10.1 HTML 是流式解析的
浏览器通常不会等待整个 HTML 下载完成后再解析。
过程可能是:
text
接收一部分 HTML
-> 解析
-> 创建 DOM
-> 发现 CSS、JavaScript、图片
-> 发起子资源请求
-> 继续接收和解析 HTML
因此,服务端流式输出和合理的 HTML 顺序可以帮助浏览器更早发现关键资源。
10.2 浏览器具有容错能力
即使 HTML 不完全规范,浏览器也会按照 HTML 标准中的错误恢复规则构建 DOM。
例如:
html
<p>第一段
<p>第二段
浏览器仍然可以生成两个段落。
但浏览器能够修复不代表开发者可以忽略规范。错误 HTML 可能导致:
- DOM 结构与源代码理解不一致。
- CSS 选择器命中异常。
- 无障碍结构错误。
- 浏览器兼容问题。
- 自动化测试不稳定。
10.3 预加载扫描器
主 HTML 解析器遇到同步脚本时可能暂停。
为了减少网络空闲,现代浏览器通常还会使用预加载扫描机制,提前扫描后续 HTML 中的:
<link><script><img><video>- 字体和其他资源
然后提前发起请求。
但如果资源地址只能通过 JavaScript 动态生成,浏览器可能无法提前发现它。
11. CSS 解析、CSSOM 与样式计算
浏览器解析 CSS 后会生成内部样式规则结构,通常可以概念性地称为 CSSOM。
例如:
css
body {
margin: 0;
font-family: system-ui, sans-serif;
}
.card {
width: 300px;
padding: 16px;
color: #1f2937;
background: #ffffff;
}
浏览器需要处理:
- 选择器匹配。
- 层叠顺序。
- 来源与重要性。
- 继承。
- 自定义属性。
- 相对单位。
- 媒体查询。
- 容器查询。
- 伪类和伪元素。
- 默认浏览器样式。
- 最终计算值。
11.1 CSS 是否阻塞 HTML 解析
外部 CSS 通常不会直接阻止 HTML 解析器继续构建 DOM,但会影响:
- 首次渲染。
- 依赖样式信息的脚本执行。
- 页面何时可以正确显示。
如果同步脚本前存在尚未加载完成的样式表,脚本可能需要等待样式表,因为脚本可能读取布局或计算样式。
11.2 层叠优先级
样式结果不仅由选择器优先级决定,还受到以下因素影响:
- 样式来源。
- 是否使用
!important。 - 级联层。
- 选择器优先级。
- 作用域接近程度。
- 声明顺序。
因此,"选择器权重越高就一定生效"是不完整的理解。
11.3 DOM 树不等于最终显示树
下面这些节点不会直接生成普通可见盒子:
css
.hidden {
display: none;
}
以下内容虽然不是普通 DOM 元素,却可能参与绘制:
css
.price::before {
content: "¥";
}
浏览器会结合 DOM、计算样式、伪元素和布局规则,生成用于布局和绘制的内部数据结构。教材中经常把它概括为 Render Tree,但具体浏览器的内部实现更加复杂。
12. JavaScript 如何影响 HTML 解析
经典同步脚本:
html
<script src="/app.js"></script>
当 HTML 解析器遇到它时,通常需要:
- 暂停 HTML 解析。
- 等待脚本下载。
- 执行脚本。
- 继续解析后续 HTML。
这是因为脚本可能调用:
javascript
document.write("<div>动态插入内容</div>");
也可能查询或修改前面的 DOM。
12.1 async、defer 和 type="module"
| 写法 | 下载方式 | 执行时机 | 顺序 |
|---|---|---|---|
普通 script |
下载时阻塞解析 | 下载完成后立即执行 | 按文档顺序 |
async |
并行下载 | 下载完成后尽快执行 | 不保证 |
defer |
并行下载 | HTML 解析完成后执行 | 通常按文档顺序 |
type="module" |
并行下载依赖图 | 默认具有延迟执行特性 | 按模块依赖关系 |
示例:
html
<script src="/legacy.js"></script>
<script async src="/analytics.js"></script>
<script defer src="/app.js"></script>
<script type="module" src="/main.js"></script>
一般建议:
- 业务主脚本使用
defer或模块脚本。 - 相互独立的统计脚本可以考虑
async。 - 必须在解析当前位置立即执行的脚本才使用普通同步脚本。
12.2 DOMContentLoaded 和 load
DOMContentLoaded 主要表示:
- HTML 已经解析完成。
- 延迟脚本和模块脚本已经按相关规则执行。
load 事件通常还要等待更多子资源完成,例如:
- 图片。
- 样式表。
- iframe。
- 部分其他外部资源。
javascript
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM 已完成解析");
});
window.addEventListener("load", () => {
console.log("页面及主要子资源加载完成");
});
页面能够交互、DOMContentLoaded 和 load 并不是同一个概念。
13. JavaScript 引擎工作原理
JavaScript 引擎并不只是逐行解释代码。
一个现代 JavaScript 引擎通常包含以下阶段:
#mermaid-svg-6GnKx4Hj1HAVEkVL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6GnKx4Hj1HAVEkVL .error-icon{fill:#552222;}#mermaid-svg-6GnKx4Hj1HAVEkVL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6GnKx4Hj1HAVEkVL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .marker.cross{stroke:#333333;}#mermaid-svg-6GnKx4Hj1HAVEkVL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6GnKx4Hj1HAVEkVL p{margin:0;}#mermaid-svg-6GnKx4Hj1HAVEkVL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .cluster-label text{fill:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .cluster-label span{color:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .cluster-label span p{background-color:transparent;}#mermaid-svg-6GnKx4Hj1HAVEkVL .label text,#mermaid-svg-6GnKx4Hj1HAVEkVL span{fill:#333;color:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .node rect,#mermaid-svg-6GnKx4Hj1HAVEkVL .node circle,#mermaid-svg-6GnKx4Hj1HAVEkVL .node ellipse,#mermaid-svg-6GnKx4Hj1HAVEkVL .node polygon,#mermaid-svg-6GnKx4Hj1HAVEkVL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .rough-node .label text,#mermaid-svg-6GnKx4Hj1HAVEkVL .node .label text,#mermaid-svg-6GnKx4Hj1HAVEkVL .image-shape .label,#mermaid-svg-6GnKx4Hj1HAVEkVL .icon-shape .label{text-anchor:middle;}#mermaid-svg-6GnKx4Hj1HAVEkVL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .rough-node .label,#mermaid-svg-6GnKx4Hj1HAVEkVL .node .label,#mermaid-svg-6GnKx4Hj1HAVEkVL .image-shape .label,#mermaid-svg-6GnKx4Hj1HAVEkVL .icon-shape .label{text-align:center;}#mermaid-svg-6GnKx4Hj1HAVEkVL .node.clickable{cursor:pointer;}#mermaid-svg-6GnKx4Hj1HAVEkVL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .arrowheadPath{fill:#333333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6GnKx4Hj1HAVEkVL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6GnKx4Hj1HAVEkVL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6GnKx4Hj1HAVEkVL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6GnKx4Hj1HAVEkVL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .cluster text{fill:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL .cluster span{color:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6GnKx4Hj1HAVEkVL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6GnKx4Hj1HAVEkVL rect.text{fill:none;stroke-width:0;}#mermaid-svg-6GnKx4Hj1HAVEkVL .icon-shape,#mermaid-svg-6GnKx4Hj1HAVEkVL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6GnKx4Hj1HAVEkVL .icon-shape p,#mermaid-svg-6GnKx4Hj1HAVEkVL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6GnKx4Hj1HAVEkVL .icon-shape .label rect,#mermaid-svg-6GnKx4Hj1HAVEkVL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6GnKx4Hj1HAVEkVL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6GnKx4Hj1HAVEkVL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6GnKx4Hj1HAVEkVL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
JavaScript 源代码
词法与语法分析
抽象语法树 AST
字节码或基线代码
执行并收集运行信息
优化编译
优化后的机器码
类型假设失效?
去优化
13.1 解析
代码:
javascript
const total = price * count;
会被分析为变量声明、标识符、乘法表达式等语法结构。
如果语法错误,解析阶段就会失败:
javascript
const total = ;
13.2 字节码和即时编译
现代引擎通常会在启动速度和运行速度之间做平衡:
- 先快速生成字节码或基线机器码。
- 执行代码并收集类型信息。
- 找出频繁执行的热点函数。
- 对热点代码进行优化编译。
- 当类型假设失效时进行去优化。
例如:
javascript
function add(a, b) {
return a + b;
}
add(1, 2);
add(3, 4);
add("hello", "world");
前两次调用可能让引擎观察到数字相加。
第三次变成字符串拼接,之前的优化假设可能不再成立。
这不意味着业务代码必须追求单一类型到极端程度,但稳定的数据结构通常更容易被引擎优化,也更容易维护。
13.3 垃圾回收
JavaScript 开发者通常不需要手动释放对象。
引擎会识别不再可达的对象并回收内存:
javascript
let user = {
id: 1001,
name: "Alice"
};
user = null;
原对象如果不存在其他引用,就可能在之后被垃圾回收。
垃圾回收通常会使用:
- 分代回收。
- 标记。
- 清除。
- 压缩。
- 增量处理。
- 并发处理。
不同引擎的具体算法不同。
13.4 内存泄漏仍然可能发生
垃圾回收只能清理不可达对象。如果对象仍然被错误引用,就不会被回收。
常见内存泄漏来源:
- 未清理的定时器。
- 未移除的事件监听器。
- 长期增长的全局数组或 Map。
- 闭包意外持有大对象。
- 已移除 DOM 节点仍被 JavaScript 引用。
- WebSocket 或订阅未关闭。
- 无限增长的前端缓存。
示例:
javascript
const handler = () => {
console.log("resize");
};
window.addEventListener("resize", handler);
// 组件销毁时应执行
window.removeEventListener("resize", handler);
14. 事件循环、任务与微任务
JavaScript 经常被描述为"单线程",更准确的说法是:
一个页面的主执行环境通常在渲染主线程上以事件循环的方式执行 JavaScript,但浏览器内部还存在网络线程、合成线程、工作线程和其他进程。
事件循环会协调:
- JavaScript 任务。
- Promise 回调。
- 用户输入。
- 网络回调。
- 定时器。
- 页面渲染。
- 垃圾回收。
- 浏览器内部任务。
14.1 任务与微任务
常见任务来源:
- 初始脚本执行。
setTimeoutsetInterval- 用户点击事件。
- 网络事件。
postMessage- 部分 I/O 回调。
常见微任务来源:
Promise.thenPromise.catchPromise.finallyqueueMicrotaskMutationObserverasync/await后续执行
示例:
javascript
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
});
console.log("D");
输出:
text
A
D
C
B
原因:
- 当前脚本作为一个任务执行。
- 输出
A。 - 注册定时器任务。
- 注册 Promise 微任务。
- 输出
D。 - 当前任务结束。
- 清空微任务队列,输出
C。 - 后续执行定时器任务,输出
B。
14.2 渲染机会
可以将事件循环简化为:
#mermaid-svg-AYwyEkVaUF1lo6Cu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AYwyEkVaUF1lo6Cu .error-icon{fill:#552222;}#mermaid-svg-AYwyEkVaUF1lo6Cu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AYwyEkVaUF1lo6Cu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .marker.cross{stroke:#333333;}#mermaid-svg-AYwyEkVaUF1lo6Cu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AYwyEkVaUF1lo6Cu p{margin:0;}#mermaid-svg-AYwyEkVaUF1lo6Cu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .cluster-label text{fill:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .cluster-label span{color:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .cluster-label span p{background-color:transparent;}#mermaid-svg-AYwyEkVaUF1lo6Cu .label text,#mermaid-svg-AYwyEkVaUF1lo6Cu span{fill:#333;color:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .node rect,#mermaid-svg-AYwyEkVaUF1lo6Cu .node circle,#mermaid-svg-AYwyEkVaUF1lo6Cu .node ellipse,#mermaid-svg-AYwyEkVaUF1lo6Cu .node polygon,#mermaid-svg-AYwyEkVaUF1lo6Cu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .rough-node .label text,#mermaid-svg-AYwyEkVaUF1lo6Cu .node .label text,#mermaid-svg-AYwyEkVaUF1lo6Cu .image-shape .label,#mermaid-svg-AYwyEkVaUF1lo6Cu .icon-shape .label{text-anchor:middle;}#mermaid-svg-AYwyEkVaUF1lo6Cu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .rough-node .label,#mermaid-svg-AYwyEkVaUF1lo6Cu .node .label,#mermaid-svg-AYwyEkVaUF1lo6Cu .image-shape .label,#mermaid-svg-AYwyEkVaUF1lo6Cu .icon-shape .label{text-align:center;}#mermaid-svg-AYwyEkVaUF1lo6Cu .node.clickable{cursor:pointer;}#mermaid-svg-AYwyEkVaUF1lo6Cu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .arrowheadPath{fill:#333333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AYwyEkVaUF1lo6Cu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AYwyEkVaUF1lo6Cu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AYwyEkVaUF1lo6Cu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AYwyEkVaUF1lo6Cu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .cluster text{fill:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu .cluster span{color:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AYwyEkVaUF1lo6Cu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AYwyEkVaUF1lo6Cu rect.text{fill:none;stroke-width:0;}#mermaid-svg-AYwyEkVaUF1lo6Cu .icon-shape,#mermaid-svg-AYwyEkVaUF1lo6Cu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AYwyEkVaUF1lo6Cu .icon-shape p,#mermaid-svg-AYwyEkVaUF1lo6Cu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AYwyEkVaUF1lo6Cu .icon-shape .label rect,#mermaid-svg-AYwyEkVaUF1lo6Cu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AYwyEkVaUF1lo6Cu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AYwyEkVaUF1lo6Cu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AYwyEkVaUF1lo6Cu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
取出一个任务
执行 JavaScript
清空微任务队列
是否存在渲染机会
执行动画帧回调和更新渲染
样式、布局、绘制、合成
等待或执行下一个任务
浏览器并不保证每执行一个任务就立即渲染。
如果 JavaScript 长时间占用主线程,页面可能无法及时:
- 响应点击。
- 处理滚动。
- 执行动画。
- 更新布局。
- 绘制新内容。
14.3 微任务也可能阻塞页面
下面的代码会不断创建微任务:
javascript
function loop() {
queueMicrotask(loop);
}
loop();
由于微任务队列持续产生新任务,浏览器可能长时间无法进入下一次渲染机会。
因此,Promise 并不意味着代码一定不会阻塞主线程。
15. 从 DOM 到屏幕像素的渲染流水线
浏览器得到 DOM 和样式规则后,需要把页面转换成屏幕上的像素。
核心过程可以概括为:
text
JavaScript
↓
Style:样式计算
↓
Layout:布局
↓
Paint:绘制
↓
Raster:栅格化
↓
Composite:合成
15.1 样式计算
浏览器为需要显示的元素计算最终样式:
css
.card {
width: 320px;
padding: 16px;
border: 1px solid #e5e7eb;
}
浏览器需要综合:
- 浏览器默认样式。
- 外部样式表。
- 内联样式。
- 继承。
- 选择器优先级。
- 级联层。
- 媒体查询。
- 当前元素状态。
- CSS 自定义属性。
15.2 布局
布局阶段计算元素的几何信息:
- 宽度。
- 高度。
- 横纵坐标。
- 行盒位置。
- 滚动区域。
- 包含块。
- 换行结果。
布局会受到以下因素影响:
- 盒模型。
- Flexbox。
- Grid。
- 普通文档流。
- 定位。
- 字体度量。
- 图片固有尺寸。
- 视口大小。
- 滚动条。
- 容器查询。
15.3 绘制
绘制阶段生成绘制指令,例如:
- 绘制背景。
- 绘制边框。
- 绘制阴影。
- 绘制文本。
- 绘制图片。
- 处理裁剪。
- 处理绘制顺序。
绘制阶段生成的通常不是最终屏幕位图,而是后续栅格化需要的绘制记录或显示列表。
15.4 栅格化
栅格化负责把绘制指令转换成像素数据。
页面可能被划分为多个图块,浏览器优先栅格化当前视口附近的内容。
栅格化任务可能由 CPU 或 GPU 相关任务协作完成,具体取决于浏览器实现、硬件和内容类型。
15.5 图层合成
浏览器不会总把整个页面绘制成一个巨大位图。
一些内容可能被分到不同合成图层,例如:
- 使用某些 CSS 变换的元素。
- 固定定位内容。
- 视频。
- Canvas。
- 需要独立滚动或裁剪的区域。
- 浏览器根据内部策略提升的元素。
合成线程将不同图层按照正确位置和顺序组合,形成最终画面。
15.6 图层不是越多越好
开发者有时会滥用:
css
.element {
will-change: transform;
}
它可能让浏览器提前为元素准备优化资源,但也可能:
- 增加内存占用。
- 增加纹理数量。
- 增加图层管理成本。
- 降低低端设备性能。
will-change 应当针对确实即将变化的元素短期使用,而不是添加到所有元素上。
16. 重排、重绘与合成
前端开发中经常出现三个概念:
- Layout/Reflow:布局或重排。
- Paint/Repaint:绘制或重绘。
- Composite:图层合成。
16.1 可能触发布局的修改
javascript
element.style.width = "400px";
element.style.height = "200px";
element.textContent = "新的内容";
这些变化可能影响元素尺寸和周围元素位置,因此可能需要重新布局。
16.2 只需要重绘的变化
javascript
element.style.backgroundColor = "red";
如果几何信息不变,可能不需要重新布局,但仍需要重新绘制。
16.3 可能主要由合成处理的变化
css
.element {
transform: translateX(100px);
opacity: 0.5;
}
对于已经具备合适合成条件的元素,transform 和 opacity 动画可能主要发生在合成阶段。
但这不是绝对保证。浏览器是否创建独立图层属于实现策略,受到元素内容、内存和设备能力影响。
16.4 强制同步布局
下面的代码可能导致布局抖动:
javascript
for (const item of items) {
item.style.width = `${item.offsetWidth + 10}px`;
}
每次循环都先读取布局,再修改样式,浏览器可能被迫反复计算布局。
更好的方式是先批量读取,再批量写入:
javascript
const widths = items.map((item) => item.offsetWidth);
items.forEach((item, index) => {
item.style.width = `${widths[index] + 10}px`;
});
原则是:
将 DOM 读取和 DOM 写入分组,减少读写交错。
17. 浏览器如何维持流畅动画
60Hz 屏幕大约每 16.67 毫秒刷新一次:
text
1000 ÷ 60 ≈ 16.67ms
120Hz 屏幕大约每 8.33 毫秒刷新一次。
这段时间不只属于 JavaScript,还要留给:
- 输入处理。
- 样式计算。
- 布局。
- 绘制。
- 合成。
- 浏览器内部任务。
因此,一个持续占用主线程 50 毫秒的任务很容易产生卡顿。
17.1 使用 requestAnimationFrame
不推荐直接使用固定间隔控制视觉动画:
javascript
setInterval(() => {
position += 2;
element.style.transform = `translateX(${position}px)`;
}, 16);
更适合使用:
javascript
let position = 0;
function animate() {
position += 2;
element.style.transform = `translateX(${position}px)`;
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
requestAnimationFrame 的优点:
- 与浏览器渲染节奏协调。
- 页面进入后台后通常会降低执行频率或暂停。
- 减少无意义的离屏绘制。
- 更适合视觉更新。
17.2 拆分长任务
不要一次处理几十万条数据:
javascript
function processAll(data) {
for (const item of data) {
expensiveWork(item);
}
}
可以分批处理:
javascript
function processInChunks(data, chunkSize = 100) {
let index = 0;
function runChunk() {
const end = Math.min(index + chunkSize, data.length);
while (index < end) {
expensiveWork(data[index]);
index += 1;
}
if (index < data.length) {
setTimeout(runChunk, 0);
}
}
runChunk();
}
更复杂的场景可以考虑:
- Web Worker。
- 服务端计算。
- 虚拟列表。
- 流式处理。
- 调度 API,但要检查目标浏览器兼容性。
18. Web Worker 与 Service Worker
18.1 Web Worker
Web Worker 允许 JavaScript 在后台线程中执行计算任务。
主线程代码:
javascript
const worker = new Worker("/worker.js", {
type: "module"
});
worker.postMessage({
numbers: [1, 2, 3, 4, 5]
});
worker.addEventListener("message", (event) => {
console.log("计算结果:", event.data);
});
worker.js:
javascript
self.addEventListener("message", (event) => {
const total = event.data.numbers.reduce((sum, value) => {
return sum + value;
}, 0);
self.postMessage(total);
});
Web Worker 适合:
- 大量数据计算。
- 图片处理。
- 文本解析。
- 加密与压缩。
- 搜索索引。
- 复杂算法。
Web Worker 通常不能直接访问页面 DOM。
18.2 Service Worker
Service Worker 是事件驱动的后台 Worker,可以参与网络请求处理。
它常用于:
- 离线访问。
- 请求缓存。
- PWA。
- 推送通知。
- 后台同步。
- 网络降级策略。
注册:
javascript
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("Service Worker 注册成功", registration.scope);
})
.catch((error) => {
console.error("Service Worker 注册失败", error);
});
}
一个简化的缓存示例:
javascript
const CACHE_NAME = "app-shell-v1";
const APP_SHELL = [
"/",
"/index.html",
"/assets/app.css",
"/assets/app.js"
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(APP_SHELL);
})
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((names) => {
return Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});
self.addEventListener("fetch", (event) => {
const request = event.request;
const url = new URL(request.url);
if (request.method !== "GET" || url.origin !== self.location.origin) {
return;
}
event.respondWith(
caches.match(request).then((cachedResponse) => {
return cachedResponse ?? fetch(request);
})
);
});
这个示例只用于说明原理。生产环境还需要考虑:
- 缓存更新。
- HTML 与静态资源采用不同策略。
- 请求失败处理。
- 离线页面。
- API 是否可以缓存。
- 登录态和用户隔离。
- 缓存容量。
- Service Worker 版本升级。
- 旧页面与新 Service Worker 的兼容性。
Service Worker 可能在空闲时被浏览器终止,不能把它当成永远运行的后台服务。
19. 同源策略与站点安全
浏览器能够运行互联网上任意来源的代码,因此必须建立严格的安全边界。
19.1 什么是源
一个源通常由以下三部分组成:
text
协议 + 主机 + 端口
例如:
text
https://www.example.com:443
下面的地址与它对比:
| 地址 | 是否同源 | 原因 |
|---|---|---|
https://www.example.com/user |
是 | 协议、主机、端口相同 |
http://www.example.com |
否 | 协议不同 |
https://api.example.com |
否 | 主机不同 |
https://www.example.com:8443 |
否 | 端口不同 |
同源不等于同站。Cookie 的 SameSite、站点隔离和同源策略使用的边界并不完全相同,工程中不能把这些概念混用。
19.2 同源策略保护什么
同源策略主要限制一个来源读取另一个来源的敏感数据,例如:
- DOM 内容。
- JavaScript 对象。
- Ajax 响应。
- LocalStorage。
- IndexedDB。
- Canvas 像素。
- 部分窗口属性。
某些跨源资源允许嵌入:
html
<img src="https://cdn.example.com/logo.png" alt="Logo">
<script src="https://cdn.example.com/library.js"></script>
<link rel="stylesheet" href="https://cdn.example.com/style.css">
但是"可以加载"不代表 JavaScript 可以读取所有内容。
19.3 CORS
CORS 是服务器通过响应头允许浏览器向指定来源暴露响应的机制。
前端请求:
javascript
const response = await fetch("https://api.example.com/users", {
credentials: "include"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json();
服务器响应:
http
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
携带凭据时不能简单使用:
http
Access-Control-Allow-Origin: *
对于非简单请求,浏览器可能先发送预检请求:
http
OPTIONS /users HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
需要特别理解:
CORS 主要控制浏览器是否允许前端 JavaScript 读取响应,不是服务器身份认证,也不能代替权限校验。
即使请求被 CORS 阻止读取,服务器仍然必须对每个请求执行认证、授权和输入校验。
19.4 XSS
XSS 是把恶意脚本注入页面。
危险代码:
javascript
result.innerHTML = userInput;
更安全的文本展示:
javascript
result.textContent = userInput;
防御措施包括:
- 默认转义不可信数据。
- 不直接拼接 HTML。
- 谨慎使用
innerHTML。 - 使用成熟的 HTML 清洗库。
- 配置 CSP。
- 避免执行字符串代码。
- 对 URL、HTML、CSS、JavaScript 上下文分别编码。
- 在支持的环境中考虑 Trusted Types。
19.5 CSRF
CSRF 利用浏览器自动携带 Cookie 的特点,诱导用户向目标网站发送请求。
防御措施:
- 使用 CSRF Token。
- Cookie 配置
SameSite。 - 校验
Origin或Referer。 - 敏感操作要求重新验证。
- 不使用 GET 修改数据。
- 为关键接口设计幂等和审计机制。
推荐的会话 Cookie 示例:
http
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
含义:
HttpOnly:减少 JavaScript 直接读取 Cookie 的风险。Secure:仅通过 HTTPS 发送。SameSite=Lax:限制部分跨站请求携带 Cookie。
19.6 CSP
CSP 可以限制页面允许加载和执行的资源。
示例:
http
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random-value';
style-src 'self';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
不要为了快速解决问题直接配置:
http
script-src 'self' 'unsafe-inline' 'unsafe-eval'
这会显著削弱 CSP 对脚本注入的防护能力。
20. 浏览器沙箱与站点隔离
渲染进程会处理来自互联网的不可信内容。
浏览器通常通过沙箱限制渲染进程访问:
- 本地文件。
- 系统注册表。
- 操作系统 API。
- 其他进程内存。
- 摄像头和麦克风。
- 用户敏感数据。
需要系统能力时,渲染进程通常通过进程间通信向高权限进程提出请求,再由浏览器进程执行权限判断。
20.1 站点隔离
站点隔离会尽可能让不同站点的文档运行在不同渲染进程中。
例如:
html
<iframe src="https://payment.example.net"></iframe>
主页面和支付 iframe 可能运行在不同渲染进程中。
这可以降低以下风险:
- 渲染器漏洞导致跨站数据泄露。
- 恶意页面直接读取其他站点进程内存。
- 某些推测执行攻击带来的跨站影响。
跨进程 iframe 仍然可以通过受限制的机制通信:
javascript
iframe.contentWindow.postMessage(
{
type: "PAYMENT_START",
orderId: "ORDER-10001"
},
"https://payment.example.net"
);
接收方必须验证来源:
javascript
window.addEventListener("message", (event) => {
if (event.origin !== "https://www.example.com") {
return;
}
if (event.data?.type === "PAYMENT_START") {
console.log("开始支付", event.data.orderId);
}
});
不要使用没有来源校验的:
javascript
window.addEventListener("message", (event) => {
executeCommand(event.data);
});
21. 前进、后退、刷新与 SPA 导航
21.1 普通页面导航
传统多页面应用导航通常会:
- 创建新的导航请求。
- 获取新的 HTML。
- 创建新的 Document。
- 构建新的 DOM。
- 执行新页面脚本。
- 替换当前页面。
21.2 SPA 导航
SPA 通常使用 History API:
javascript
history.pushState(
{
page: "detail"
},
"",
"/products/100"
);
这会改变地址栏和历史记录,但不会自动向服务器请求新的 HTML。
监听历史导航:
javascript
window.addEventListener("popstate", (event) => {
console.log("历史状态变化", event.state);
renderCurrentRoute();
});
SPA 路由切换仍然需要处理:
- 数据请求。
- 组件销毁。
- 事件解绑。
- 滚动位置。
- 页面标题。
- 焦点管理。
- 无障碍播报。
- 错误边界。
- 缓存。
- 性能指标。
21.3 普通刷新与强制刷新
不同浏览器和系统快捷键存在差异,概念上可以理解为:
- 普通刷新:允许按正常缓存规则验证或使用缓存。
- 强制刷新:更积极地绕过部分缓存。
- DevTools Disable cache:通常只在开发者工具打开时生效。
调试缓存问题时,应同时检查:
- Network 中的 Size。
from memory cache。from disk cache。- Service Worker。
Cache-Control。Age。ETag。- CDN 响应头。
- 请求是否真的到达服务器。
22. Web 性能的核心指标
当前常用的 Core Web Vitals 包括:
| 指标 | 衡量内容 | 良好标准 |
|---|---|---|
| LCP | 主要内容加载速度 | 不高于 2.5 秒 |
| INP | 页面交互响应能力 | 不高于 200 毫秒 |
| CLS | 页面视觉稳定性 | 不高于 0.1 |
通常应关注真实用户访问数据的第 75 百分位,而不是只看开发者电脑上的一次测试。
22.1 LCP
LCP 即 Largest Contentful Paint,关注视口中主要内容何时显示。
LCP 元素常见于:
- 首屏大图。
- Banner 图片。
- 大标题。
- 主要文本块。
- 视频封面。
优化方向:
- 减少服务器响应时间。
- 尽早发现 LCP 资源。
- 避免对首屏主图使用懒加载。
- 压缩图片。
- 使用合适格式。
- 提供响应式图片。
- 减少阻塞渲染的 CSS 和 JavaScript。
- 合理使用 CDN。
- 对明确的 LCP 图片设置较高获取优先级。
示例:
html
<link
rel="preload"
as="image"
href="/images/hero.avif"
imagesrcset="
/images/hero-800.avif 800w,
/images/hero-1600.avif 1600w
"
imagesizes="100vw"
>
<img
src="/images/hero-1600.avif"
srcset="
/images/hero-800.avif 800w,
/images/hero-1600.avif 1600w
"
sizes="100vw"
width="1600"
height="900"
fetchpriority="high"
alt="浏览器工作原理示意图"
>
不要同时预加载大量图片,否则关键资源会与非关键资源竞争带宽。
22.2 INP
INP 即 Interaction to Next Paint,关注用户交互到下一次页面绘制的延迟。
交互延迟可能来自:
- 主线程正在执行长任务。
- 事件处理器逻辑复杂。
- React、Vue 等框架同步更新范围过大。
- DOM 节点过多。
- 布局计算复杂。
- 第三方脚本占用主线程。
- 点击后同步解析大量 JSON。
- 输入事件触发频繁计算。
优化方向:
- 缩短事件处理器。
- 将非关键逻辑延后。
- 拆分长任务。
- 减少不必要渲染。
- 使用虚拟列表。
- 使用 Web Worker。
- 减少第三方脚本。
- 避免频繁同步布局。
22.3 CLS
CLS 即 Cumulative Layout Shift,衡量页面意外位移。
常见原因:
- 图片没有声明尺寸。
- 广告位动态插入但没有预留空间。
- 字体切换导致文本尺寸变化。
- 异步内容插入页面顶部。
- 动画修改影响布局的属性。
为图片声明尺寸:
html
<img
src="/images/product.webp"
width="800"
height="600"
alt="商品图片"
>
为动态区域预留空间:
css
.ad-slot {
min-height: 250px;
}
尽量使用不会推动其他元素布局的视觉动画。
23. 使用 Performance API 分析页面
23.1 导航耗时
javascript
const navigation = performance.getEntriesByType("navigation")[0];
if (navigation) {
const result = {
redirect: navigation.redirectEnd - navigation.redirectStart,
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
connection: navigation.connectEnd - navigation.connectStart,
requestToFirstByte:
navigation.responseStart - navigation.requestStart,
download:
navigation.responseEnd - navigation.responseStart,
domInteractive:
navigation.domInteractive - navigation.startTime,
domContentLoaded:
navigation.domContentLoadedEventEnd - navigation.startTime,
load:
navigation.loadEventEnd - navigation.startTime
};
console.table(result);
}
需要注意:
- 连接被复用时 DNS 和连接耗时可能是 0。
- Service Worker 会改变请求路径。
- 缓存命中会改变各阶段耗时。
- 跨域资源的详细时间可能受
Timing-Allow-Origin限制。 - 单次数据不能代表真实用户总体表现。
23.2 资源加载分析
javascript
const resources = performance.getEntriesByType("resource");
const slowResources = resources
.filter((entry) => entry.duration > 500)
.map((entry) => ({
name: entry.name,
type: entry.initiatorType,
duration: Math.round(entry.duration),
transferSize: entry.transferSize,
decodedBodySize: entry.decodedBodySize
}));
console.table(slowResources);
23.3 真实用户性能监控
生产环境推荐使用经过兼容性处理的 web-vitals 库采集指标:
bash
npm install web-vitals
javascript
import {
onCLS,
onINP,
onLCP
} from "web-vitals";
function reportMetric(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
path: location.pathname,
timestamp: Date.now()
});
navigator.sendBeacon(
"/api/metrics",
new Blob([body], {
type: "application/json"
})
);
}
onCLS(reportMetric);
onINP(reportMetric);
onLCP(reportMetric);
生产监控还应按以下维度分析:
- 页面。
- 设备类型。
- 操作系统。
- 浏览器版本。
- 网络类型。
- 地区。
- 应用版本。
- 用户是否登录。
- 第三方脚本版本。
监控时应避免采集用户敏感信息。
24. 浏览器开发者工具实战
Chrome、Edge、Firefox 和 Safari 都提供开发者工具。
24.1 Network 面板
重点观察:
- 请求数量。
- 请求方法。
- 状态码。
- 协议版本。
- 资源类型。
- 请求优先级。
- DNS 时间。
- 连接时间。
- TLS 时间。
- TTFB。
- 内容下载时间。
- 是否命中缓存。
- 请求发起者。
- 响应头。
- 请求体。
- Cookie。
- 重定向链。
常见现象:
| 现象 | 可能原因 |
|---|---|
| Waiting/TTFB 很长 | 服务端慢、数据库慢、上游慢、网络延迟 |
| Content Download 很长 | 文件过大、带宽低、未压缩 |
| Queued/Stalled 很长 | 连接限制、请求优先级、代理、主线程调度 |
| DNS 很长 | DNS 服务慢、缓存未命中 |
| SSL 很长 | TLS 握手、网络往返、证书链问题 |
| 304 仍然很慢 | 验证请求仍需访问服务器 |
| 请求显示 from ServiceWorker | 请求被 Service Worker 处理 |
| 接口成功但前端报 CORS | 响应未满足浏览器跨域读取规则 |
24.2 Performance 面板
Performance 面板可用于分析:
- 主线程任务。
- JavaScript 执行。
- 样式计算。
- 布局。
- 绘制。
- 图层合成。
- 长任务。
- 用户交互。
- 动画帧。
- 内存变化。
分析卡顿的一般步骤:
- 录制问题操作。
- 找到超过 50ms 的长任务。
- 展开调用栈。
- 判断是脚本、样式还是布局。
- 查看是否存在频繁布局。
- 检查第三方脚本。
- 修改代码。
- 使用相同环境重新录制。
24.3 Memory 面板
Memory 面板可以分析:
- JavaScript 堆快照。
- 对象引用关系。
- Detached DOM。
- 持续增长的对象。
- 内存分配时间线。
排查内存泄漏时可以:
- 打开目标页面。
- 执行一次垃圾回收。
- 获取堆快照。
- 重复进入和离开目标页面。
- 再次执行垃圾回收。
- 获取第二个快照。
- 对比对象数量和保留路径。
24.4 Rendering 工具
可以开启:
- Paint flashing。
- Layout Shift Regions。
- FPS Meter。
- Layer borders。
- 滚动性能提示。
这些工具能帮助判断页面是否频繁重绘、发生布局偏移或创建过多图层。
25. 工程中的性能优化策略
性能优化不能只靠压缩 JavaScript,需要覆盖完整关键路径。
25.1 HTML
- 尽早输出关键内容。
- 将字符编码声明放在前面。
- 保持 DOM 结构合理。
- 避免无意义的深层嵌套。
- 首屏关键资源应尽早被 HTML 发现。
- 为图片提供宽高。
- 延迟加载非首屏图片和 iframe。
html
<img
src="/images/article-cover.webp"
width="1200"
height="675"
loading="lazy"
decoding="async"
alt="文章封面"
>
不要为首屏 LCP 图片使用 loading="lazy"。
25.2 CSS
- 提取首屏关键 CSS时要控制复杂度。
- 延迟非关键样式。
- 删除长期未使用样式。
- 避免过度复杂的选择器。
- 控制阴影、滤镜和模糊效果。
- 避免 CSS 文件无限增长。
- 谨慎使用
content-visibility和contain。 - 评估字体加载对布局的影响。
25.3 JavaScript
- 使用代码分割。
- 路由级按需加载。
- 删除无用依赖。
- 减少首屏执行代码。
- 对第三方脚本设置加载边界。
- 避免在主线程执行重计算。
- 避免频繁创建和销毁大量 DOM。
- 使用事件委托。
- 及时清理监听器和订阅。
动态导入示例:
javascript
button.addEventListener("click", async () => {
const {
openEditor
} = await import("./editor.js");
openEditor();
});
25.4 图片
- 根据场景选择 AVIF、WebP、PNG、JPEG 或 SVG。
- 使用
srcset。 - 提供正确尺寸。
- 避免下载远大于显示尺寸的图片。
- 首屏主图不要懒加载。
- 非首屏图片使用懒加载。
- 对上传图片进行服务端压缩。
- CDN 根据设备和格式协商输出。
25.5 字体
- 优先使用系统字体时不必下载字体。
- 对自定义字体进行子集化。
- 使用 WOFF2。
- 控制字体字重数量。
- 合理配置
font-display。 - 只预加载真正关键的字体。
css
@font-face {
font-family: "BlogFont";
src: url("/assets/blog-font.woff2") format("woff2");
font-display: swap;
font-weight: 400;
font-style: normal;
}
25.6 服务器
- 降低应用响应时间。
- 优化数据库查询。
- 使用 CDN。
- 开启 Brotli 或 Gzip。
- 正确配置缓存头。
- 启用 HTTP/2 或 HTTP/3。
- 避免多级重定向。
- 对接口设置超时。
- 对静态资源使用内容哈希。
- 做好限流和熔断。
26. 静态资源缓存配置实践
假设前端构建产物如下:
text
dist/
├── index.html
└── assets/
├── app.9f3ca21.js
├── app.4428ab1.css
└── logo.a83f122.webp
其中 assets 下的资源名包含内容哈希,可以长期缓存。
Nginx 示例:
nginx
server {
listen 443 ssl;
server_name www.example.com;
root /var/www/app;
location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location = /index.html {
add_header Cache-Control "no-cache";
}
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
application/javascript
application/json
application/xml
image/svg+xml;
}
这套策略的核心是:
text
HTML:短缓存或每次验证
带哈希静态资源:长期强缓存
如果静态资源文件名不包含内容哈希,就不能直接设置一年不可变缓存,否则发布新版本后用户可能继续使用旧文件。
27. 建立性能预算
性能优化应从"感觉更快"变成可验证的工程指标。
可以为项目定义示例预算:
| 项目 | 示例预算 |
|---|---|
| HTML 压缩后大小 | 不超过 30KB |
| 首屏 CSS 压缩后大小 | 不超过 40KB |
| 首屏 JavaScript 压缩后大小 | 不超过 200KB |
| 首屏关键图片 | 不超过 200KB |
| 第三方脚本 | 不超过 3 个关键来源 |
| LCP | 第 75 百分位不高于 2.5s |
| INP | 第 75 百分位不高于 200ms |
| CLS | 第 75 百分位不高于 0.1 |
这些数字只是示例,不是所有项目的统一标准。
实际预算应根据:
- 用户设备。
- 网络环境。
- 产品类型。
- 交互复杂度。
- 业务价值。
- 历史监控数据。
27.1 CI 中检查资源大小
示例 Node.js 脚本:
javascript
import {
stat
} from "node:fs/promises";
const budgets = [
{
file: "dist/assets/app.js",
maxBytes: 200 * 1024
},
{
file: "dist/assets/app.css",
maxBytes: 40 * 1024
}
];
let failed = false;
for (const budget of budgets) {
const fileStat = await stat(budget.file);
if (fileStat.size > budget.maxBytes) {
failed = true;
console.error(
`${budget.file} 超出预算:` +
`${fileStat.size} > ${budget.maxBytes}`
);
}
}
if (failed) {
process.exitCode = 1;
}
生产项目应检查压缩后的真实传输大小,并结合构建工具的产物清单。
28. 跨浏览器测试
Chrome 测试通过不代表所有浏览器都能正常运行。
工程中至少应关注:
- Chromium。
- Firefox。
- WebKit/Safari。
- 移动端视口。
- 触摸交互。
- 键盘操作。
- 屏幕阅读器相关语义。
- 低性能设备。
- 弱网环境。
- 深色模式。
- 缩放和高分屏。
Playwright 示例:
javascript
import {
test,
expect
} from "@playwright/test";
test("首页可以正常加载和导航", async ({
page
}) => {
await page.goto("http://127.0.0.1:4173/");
await expect(
page.getByRole("heading", {
name: "浏览器工作原理"
})
).toBeVisible();
await page
.getByRole("link", {
name: "开始阅读"
})
.click();
await expect(page).toHaveURL(/\/article/);
});
playwright.config.js:
javascript
import {
defineConfig,
devices
} from "@playwright/test";
export default defineConfig({
use: {
baseURL: "http://127.0.0.1:4173"
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"]
}
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"]
}
},
{
name: "webkit",
use: {
...devices["Desktop Safari"]
}
}
]
});
自动化测试不能完全代替真实 Safari、移动设备和辅助技术测试,但可以尽早发现大量兼容性问题。
29. 一个系统化的性能排查流程
遇到"网页很慢"时,不要直接开始修改代码。
推荐按以下流程排查。
第一步:明确问题
先判断是哪一种慢:
- 首次打开慢。
- 刷新慢。
- 接口慢。
- 图片慢。
- 点击响应慢。
- 滚动卡顿。
- 动画掉帧。
- 路由切换慢。
- 页面运行一段时间后变慢。
- 只有部分地区或设备慢。
第二步:建立可复现环境
记录:
- 页面 URL。
- 浏览器和版本。
- 操作系统。
- 设备型号。
- 网络环境。
- 登录状态。
- 应用版本。
- 复现步骤。
- 问题发生概率。
第三步:收集证据
使用:
- Network。
- Performance。
- Memory。
- Lighthouse。
- 服务端日志。
- APM。
- CDN 日志。
- 真实用户监控。
- 错误监控。
第四步:定位瓶颈层次
#mermaid-svg-wIydmJ6Pyjax1T4c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wIydmJ6Pyjax1T4c .error-icon{fill:#552222;}#mermaid-svg-wIydmJ6Pyjax1T4c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wIydmJ6Pyjax1T4c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wIydmJ6Pyjax1T4c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wIydmJ6Pyjax1T4c .marker.cross{stroke:#333333;}#mermaid-svg-wIydmJ6Pyjax1T4c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wIydmJ6Pyjax1T4c p{margin:0;}#mermaid-svg-wIydmJ6Pyjax1T4c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c .cluster-label text{fill:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c .cluster-label span{color:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c .cluster-label span p{background-color:transparent;}#mermaid-svg-wIydmJ6Pyjax1T4c .label text,#mermaid-svg-wIydmJ6Pyjax1T4c span{fill:#333;color:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c .node rect,#mermaid-svg-wIydmJ6Pyjax1T4c .node circle,#mermaid-svg-wIydmJ6Pyjax1T4c .node ellipse,#mermaid-svg-wIydmJ6Pyjax1T4c .node polygon,#mermaid-svg-wIydmJ6Pyjax1T4c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wIydmJ6Pyjax1T4c .rough-node .label text,#mermaid-svg-wIydmJ6Pyjax1T4c .node .label text,#mermaid-svg-wIydmJ6Pyjax1T4c .image-shape .label,#mermaid-svg-wIydmJ6Pyjax1T4c .icon-shape .label{text-anchor:middle;}#mermaid-svg-wIydmJ6Pyjax1T4c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wIydmJ6Pyjax1T4c .rough-node .label,#mermaid-svg-wIydmJ6Pyjax1T4c .node .label,#mermaid-svg-wIydmJ6Pyjax1T4c .image-shape .label,#mermaid-svg-wIydmJ6Pyjax1T4c .icon-shape .label{text-align:center;}#mermaid-svg-wIydmJ6Pyjax1T4c .node.clickable{cursor:pointer;}#mermaid-svg-wIydmJ6Pyjax1T4c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wIydmJ6Pyjax1T4c .arrowheadPath{fill:#333333;}#mermaid-svg-wIydmJ6Pyjax1T4c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wIydmJ6Pyjax1T4c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wIydmJ6Pyjax1T4c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wIydmJ6Pyjax1T4c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wIydmJ6Pyjax1T4c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wIydmJ6Pyjax1T4c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wIydmJ6Pyjax1T4c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wIydmJ6Pyjax1T4c .cluster text{fill:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c .cluster span{color:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wIydmJ6Pyjax1T4c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wIydmJ6Pyjax1T4c rect.text{fill:none;stroke-width:0;}#mermaid-svg-wIydmJ6Pyjax1T4c .icon-shape,#mermaid-svg-wIydmJ6Pyjax1T4c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wIydmJ6Pyjax1T4c .icon-shape p,#mermaid-svg-wIydmJ6Pyjax1T4c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wIydmJ6Pyjax1T4c .icon-shape .label rect,#mermaid-svg-wIydmJ6Pyjax1T4c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wIydmJ6Pyjax1T4c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wIydmJ6Pyjax1T4c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wIydmJ6Pyjax1T4c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
是
否
否
是
否
是
否
是
否
页面慢
网络请求慢?
TTFB 慢?
服务端、数据库、上游、网络
文件过大、带宽、压缩、缓存
主线程长任务?
JavaScript、第三方脚本、框架更新
布局或绘制耗时?
DOM、CSS、布局抖动、复杂视觉效果
内存持续增长?
监听器、缓存、Detached DOM、订阅
检查设备、扩展、GPU、浏览器差异
第五步:一次只验证一个核心假设
例如:
- 假设主图过大导致 LCP 慢。
- 假设第三方脚本造成长任务。
- 假设接口 TTFB 过高。
- 假设列表一次渲染一万条数据。
- 假设缓存头配置错误。
每次修改后使用相同条件重新测试。
第六步:验证真实用户效果
实验室数据变好后,还要观察:
- 真实用户第 75 百分位。
- 不同设备和网络。
- 错误率。
- 转化率。
- 流量和服务器成本。
- 是否引入兼容性问题。
30. 常见错误认识
30.1 JavaScript 是单线程,所以浏览器只有一个线程
错误。
JavaScript 主执行环境通常在主线程上运行,但浏览器还包含:
- 网络线程。
- 合成线程。
- 栅格线程。
- I/O 线程。
- GPU 进程。
- Web Worker。
- Service Worker。
- 音视频线程。
30.2 CSS 会阻塞 DOM 构建
不完全准确。
CSS 通常不会直接让 HTML 解析器停止构建 DOM,但它会阻塞或影响首次正确渲染,并可能间接延迟依赖样式信息的脚本。
30.3 setTimeout(fn, 0) 会立即执行
错误。
它只是把回调安排到后续任务中。只有当前任务结束、微任务处理完成并且事件循环调度到它时,回调才可能执行。
30.4 Promise 一定不会阻塞页面
错误。
Promise 回调仍然执行 JavaScript。如果回调计算量大,或者持续创建微任务,仍然会阻塞主线程。
30.5 HTTP/2 一定比 HTTP/1.1 快
不一定。
性能还取决于:
- 网络质量。
- TLS。
- 服务器实现。
- 资源数量。
- 资源优先级。
- CDN。
- 丢包率。
- 连接复用。
- 缓存。
30.6 HTTP/3 完全消除了队头阻塞
错误。
HTTP/3 改善了 TCP 连接级别的跨流队头阻塞,但单个 QUIC 流内部仍然需要可靠、有序交付。
30.7 CORS 能保护接口不被别人调用
错误。
CORS 是浏览器读取限制,不是接口认证机制。非浏览器客户端不受普通浏览器 CORS 策略约束。
30.8 使用 GPU 一定更快
错误。
创建过多图层会增加:
- 显存占用。
- 栅格化成本。
- 合成成本。
- 图层同步成本。
30.9 页面 load 完成就代表性能很好
错误。
页面可能已经触发 load,但仍然:
- 无法及时交互。
- 存在长任务。
- 持续发生布局偏移。
- 第三方脚本仍在执行。
- 用户点击后响应很慢。
30.10 Lighthouse 100 分代表所有用户都快
错误。
Lighthouse 主要是受控环境中的实验室测试。真实用户还受到设备、网络、地区、缓存状态和使用方式影响。
31. 浏览器原理常见面试题
31.1 输入 URL 后发生了什么
可以按以下顺序回答:
- URL 解析。
- 导航策略检查。
- Service Worker 和缓存检查。
- DNS 解析。
- 建立 TCP 或 QUIC 连接。
- TLS 握手。
- 发送 HTTP 请求。
- 服务器处理并返回响应。
- 浏览器处理状态码和响应头。
- 流式解析 HTML。
- 加载 CSS、JavaScript、图片和字体。
- 构建 DOM 和样式相关结构。
- 样式计算。
- 布局。
- 绘制。
- 栅格化。
- 图层合成。
- 页面显示和后续交互。
同时说明这些过程存在大量缓存、并行和浏览器实现差异。
31.2 为什么 JavaScript 会阻塞 HTML 解析
因为脚本可能读取或修改当前文档,甚至使用 document.write 改变后续解析结果,所以经典同步脚本通常需要暂停 HTML 解析。
31.3 defer 和 async 有什么区别
- 两者都可以并行下载。
async下载完成后尽快执行,不保证顺序。defer通常在 HTML 解析完成后按顺序执行。- 业务脚本通常更适合
defer。 - 独立统计脚本可能适合
async。
31.4 重排和重绘有什么区别
- 重排重新计算几何位置。
- 重绘重新生成视觉绘制内容。
- 合成重新组合图层。
- 重排通常会带来后续绘制和合成工作。
- 修改
transform、opacity在合适条件下可能主要影响合成。
31.5 为什么操作 DOM 很慢
不是 DOM API 本身必然很慢,而是操作可能触发:
- 样式失效。
- 布局。
- 绘制。
- 合成。
- 大量节点创建和销毁。
- JavaScript 与渲染流水线同步。
31.6 什么是事件循环
事件循环是浏览器协调任务、微任务、用户输入、网络回调和页面渲染的调度机制。
31.7 为什么跨域请求会失败
常见原因是响应不满足 CORS 规则,例如:
- 缺少
Access-Control-Allow-Origin。 - 预检请求失败。
- 携带凭据却使用通配符来源。
- 请求头或方法未被允许。
- 重定向后的响应不满足规则。
32. 从原理到工程实践的完整检查清单
网络层
- 是否存在不必要重定向。
- DNS 和连接是否耗时异常。
- 是否启用 HTTPS。
- 是否正确使用 HTTP/2 或 HTTP/3。
- 是否开启压缩。
- 是否正确配置 CDN。
- TTFB 是否稳定。
- 是否存在过多跨域连接。
- 是否设置接口超时。
- 是否正确处理请求取消。
缓存层
- HTML 是否使用短缓存或协商缓存。
- 带哈希静态资源是否长期缓存。
- 是否区分
no-cache与no-store。 - CDN 与浏览器缓存策略是否一致。
- Service Worker 是否存在旧缓存问题。
- 登录数据是否被错误缓存。
- 是否测试前进和后退场景。
HTML 与资源发现
- 关键内容是否尽早出现在 HTML 中。
- 字符编码是否尽早声明。
- 首屏主图是否可被预加载扫描器发现。
- 图片是否声明宽高。
- 非首屏资源是否延迟加载。
- 是否避免无效预加载。
- DOM 是否过深或过大。
CSS
- 是否存在大量未使用 CSS。
- 是否存在阻塞首屏的大样式表。
- 是否使用过度复杂选择器。
- 是否存在频繁布局。
- 是否滥用阴影、滤镜和模糊。
- 是否滥用
will-change。 - 字体加载是否导致布局偏移。
JavaScript
- 是否存在同步阻塞脚本。
- 是否使用
defer或模块脚本。 - 首屏 JavaScript 是否过大。
- 是否存在超过 50ms 的长任务。
- 是否存在无意义重复渲染。
- 是否存在微任务饥饿。
- 是否可以使用 Web Worker。
- 是否及时清理事件、定时器和订阅。
- 第三方脚本是否可控。
安全
- 是否全站使用 HTTPS。
- Cookie 是否配置
HttpOnly、Secure和合适的SameSite。 - 是否正确配置 CORS。
- 服务端是否独立执行认证和授权。
- 是否防御 XSS。
- 是否防御 CSRF。
- 是否配置 CSP。
-
postMessage是否校验origin。 - iframe 是否配置必要的沙箱和权限。
- 是否避免把密钥放在前端代码中。
可观测性
- 是否采集 LCP、INP 和 CLS。
- 是否区分实验室数据与真实用户数据。
- 是否记录应用版本。
- 是否按设备、网络、地区分析。
- 是否监控静态资源加载失败。
- 是否监控接口错误和超时。
- 是否设置性能预算。
- 是否在发布前后进行对比。
33. 总结
浏览器显示一个网页,远不是"发送请求并执行 JavaScript"这么简单。
整个过程包含:
text
用户输入 URL
↓
导航与安全检查
↓
DNS 解析
↓
建立 TCP 或 QUIC 连接
↓
TLS 握手
↓
发送 HTTP 请求
↓
缓存或 Service Worker 处理
↓
接收 HTTP 响应
↓
解析 HTML
↓
加载 CSS、JavaScript、图片和字体
↓
构建 DOM 和样式数据
↓
执行 JavaScript
↓
样式计算
↓
布局
↓
绘制
↓
栅格化
↓
图层合成
↓
屏幕显示
真正理解浏览器工作原理后,许多前端问题都会变得更容易解释:
- 为什么同步脚本会阻塞页面。
- 为什么 Promise 仍然可能造成卡顿。
- 为什么读取布局后再修改样式容易产生布局抖动。
- 为什么首屏大图不应该懒加载。
- 为什么
no-cache不等于不缓存。 - 为什么 CORS 不能代替接口鉴权。
- 为什么页面需要多进程和站点隔离。
- 为什么实验室跑分不能代替真实用户监控。
- 为什么性能优化必须覆盖网络、服务端、资源和渲染完整链路。
浏览器原理最终不是为了背诵名词,而是为了建立一套完整的问题定位模型:
先判断问题发生在导航、网络、缓存、解析、脚本、布局、绘制、合成、安全还是服务端,再使用数据验证假设,实施最小改动,最后通过真实用户指标确认效果。