浏览器工作原理详解:从入门到工程实践

浏览器看起来只是一个"打开网页"的工具,内部却同时包含网络协议栈、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 浏览器开始导航前会做什么

浏览器进程通常会进行以下检查:

  1. 判断输入内容是 URL 还是搜索词。
  2. 对 URL 进行标准化。
  3. 检查是否需要从 HTTP 升级到 HTTPS。
  4. 检查企业代理、安全策略和扩展拦截规则。
  5. 判断是否为页面内 Fragment 导航。
  6. 判断是否为浏览器内部页面。
  7. 检查当前页面是否注册了离开确认。
  8. 选择或创建合适的渲染进程。
  9. 创建导航请求。
  10. 将请求交给网络服务或 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 预检

浏览器还可能自动添加:

  • Cookie
  • Origin
  • Referer
  • Sec-Fetch-Site
  • Sec-Fetch-Mode
  • Sec-Fetch-Dest
  • Accept-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-cacheno-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 字节流后,会进行:

  1. 字节解码。
  2. 字符流处理。
  3. 词法分析。
  4. Token 生成。
  5. 树构建。
  6. 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 层叠优先级

样式结果不仅由选择器优先级决定,还受到以下因素影响:

  1. 样式来源。
  2. 是否使用 !important
  3. 级联层。
  4. 选择器优先级。
  5. 作用域接近程度。
  6. 声明顺序。

因此,"选择器权重越高就一定生效"是不完整的理解。

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 解析器遇到它时,通常需要:

  1. 暂停 HTML 解析。
  2. 等待脚本下载。
  3. 执行脚本。
  4. 继续解析后续 HTML。

这是因为脚本可能调用:

javascript 复制代码
document.write("<div>动态插入内容</div>");

也可能查询或修改前面的 DOM。

12.1 asyncdefertype="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 DOMContentLoadedload

DOMContentLoaded 主要表示:

  • HTML 已经解析完成。
  • 延迟脚本和模块脚本已经按相关规则执行。

load 事件通常还要等待更多子资源完成,例如:

  • 图片。
  • 样式表。
  • iframe。
  • 部分其他外部资源。
javascript 复制代码
document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM 已完成解析");
});

window.addEventListener("load", () => {
  console.log("页面及主要子资源加载完成");
});

页面能够交互、DOMContentLoadedload 并不是同一个概念。


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 任务与微任务

常见任务来源:

  • 初始脚本执行。
  • setTimeout
  • setInterval
  • 用户点击事件。
  • 网络事件。
  • postMessage
  • 部分 I/O 回调。

常见微任务来源:

  • Promise.then
  • Promise.catch
  • Promise.finally
  • queueMicrotask
  • MutationObserver
  • async/await 后续执行

示例:

javascript 复制代码
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");

输出:

text 复制代码
A
D
C
B

原因:

  1. 当前脚本作为一个任务执行。
  2. 输出 A
  3. 注册定时器任务。
  4. 注册 Promise 微任务。
  5. 输出 D
  6. 当前任务结束。
  7. 清空微任务队列,输出 C
  8. 后续执行定时器任务,输出 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;
}

对于已经具备合适合成条件的元素,transformopacity 动画可能主要发生在合成阶段。

但这不是绝对保证。浏览器是否创建独立图层属于实现策略,受到元素内容、内存和设备能力影响。

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
  • 校验 OriginReferer
  • 敏感操作要求重新验证。
  • 不使用 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 普通页面导航

传统多页面应用导航通常会:

  1. 创建新的导航请求。
  2. 获取新的 HTML。
  3. 创建新的 Document。
  4. 构建新的 DOM。
  5. 执行新页面脚本。
  6. 替换当前页面。

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 执行。
  • 样式计算。
  • 布局。
  • 绘制。
  • 图层合成。
  • 长任务。
  • 用户交互。
  • 动画帧。
  • 内存变化。

分析卡顿的一般步骤:

  1. 录制问题操作。
  2. 找到超过 50ms 的长任务。
  3. 展开调用栈。
  4. 判断是脚本、样式还是布局。
  5. 查看是否存在频繁布局。
  6. 检查第三方脚本。
  7. 修改代码。
  8. 使用相同环境重新录制。

24.3 Memory 面板

Memory 面板可以分析:

  • JavaScript 堆快照。
  • 对象引用关系。
  • Detached DOM。
  • 持续增长的对象。
  • 内存分配时间线。

排查内存泄漏时可以:

  1. 打开目标页面。
  2. 执行一次垃圾回收。
  3. 获取堆快照。
  4. 重复进入和离开目标页面。
  5. 再次执行垃圾回收。
  6. 获取第二个快照。
  7. 对比对象数量和保留路径。

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-visibilitycontain
  • 评估字体加载对布局的影响。

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 后发生了什么

可以按以下顺序回答:

  1. URL 解析。
  2. 导航策略检查。
  3. Service Worker 和缓存检查。
  4. DNS 解析。
  5. 建立 TCP 或 QUIC 连接。
  6. TLS 握手。
  7. 发送 HTTP 请求。
  8. 服务器处理并返回响应。
  9. 浏览器处理状态码和响应头。
  10. 流式解析 HTML。
  11. 加载 CSS、JavaScript、图片和字体。
  12. 构建 DOM 和样式相关结构。
  13. 样式计算。
  14. 布局。
  15. 绘制。
  16. 栅格化。
  17. 图层合成。
  18. 页面显示和后续交互。

同时说明这些过程存在大量缓存、并行和浏览器实现差异。

31.2 为什么 JavaScript 会阻塞 HTML 解析

因为脚本可能读取或修改当前文档,甚至使用 document.write 改变后续解析结果,所以经典同步脚本通常需要暂停 HTML 解析。

31.3 deferasync 有什么区别

  • 两者都可以并行下载。
  • async 下载完成后尽快执行,不保证顺序。
  • defer 通常在 HTML 解析完成后按顺序执行。
  • 业务脚本通常更适合 defer
  • 独立统计脚本可能适合 async

31.4 重排和重绘有什么区别

  • 重排重新计算几何位置。
  • 重绘重新生成视觉绘制内容。
  • 合成重新组合图层。
  • 重排通常会带来后续绘制和合成工作。
  • 修改 transformopacity 在合适条件下可能主要影响合成。

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-cacheno-store
  • CDN 与浏览器缓存策略是否一致。
  • Service Worker 是否存在旧缓存问题。
  • 登录数据是否被错误缓存。
  • 是否测试前进和后退场景。

HTML 与资源发现

  • 关键内容是否尽早出现在 HTML 中。
  • 字符编码是否尽早声明。
  • 首屏主图是否可被预加载扫描器发现。
  • 图片是否声明宽高。
  • 非首屏资源是否延迟加载。
  • 是否避免无效预加载。
  • DOM 是否过深或过大。

CSS

  • 是否存在大量未使用 CSS。
  • 是否存在阻塞首屏的大样式表。
  • 是否使用过度复杂选择器。
  • 是否存在频繁布局。
  • 是否滥用阴影、滤镜和模糊。
  • 是否滥用 will-change
  • 字体加载是否导致布局偏移。

JavaScript

  • 是否存在同步阻塞脚本。
  • 是否使用 defer 或模块脚本。
  • 首屏 JavaScript 是否过大。
  • 是否存在超过 50ms 的长任务。
  • 是否存在无意义重复渲染。
  • 是否存在微任务饥饿。
  • 是否可以使用 Web Worker。
  • 是否及时清理事件、定时器和订阅。
  • 第三方脚本是否可控。

安全

  • 是否全站使用 HTTPS。
  • Cookie 是否配置 HttpOnlySecure 和合适的 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 不能代替接口鉴权。
  • 为什么页面需要多进程和站点隔离。
  • 为什么实验室跑分不能代替真实用户监控。
  • 为什么性能优化必须覆盖网络、服务端、资源和渲染完整链路。

浏览器原理最终不是为了背诵名词,而是为了建立一套完整的问题定位模型:

先判断问题发生在导航、网络、缓存、解析、脚本、布局、绘制、合成、安全还是服务端,再使用数据验证假设,实施最小改动,最后通过真实用户指标确认效果。


34. 参考资料

相关推荐
梦想blog1 天前
Claude Code + Chrome MCP:让浏览器自动化测试更简单
前端·chrome·自动化·mcp
Codiggerworld2 天前
Chrome DevTools 隐藏神技:这 5 个调试技巧你可能从来没用过
前端·chrome·chrome devtools
RisunJan2 天前
Linux命令-whoami(显示当前有效用户名)
linux·运维·chrome
不可求~2 天前
调用 GPT、Claude、Gemini API 报错怎么办:一套通用排查清单
网络·chrome·gpt
fb_123452 天前
Shell 脚本从 0 到精通|脚本规范 + 变量 + 数值运算 + 条件测试(可直接复制,面试必备)
chrome·面试·职场和发展
就叫飞六吧3 天前
两道门:X-Frame-Options 和 SameSite 到底谁管什么
开发语言·chrome·ai编程
RisunJan3 天前
Linux命令-wait(Bash 内建 —— 等待后台进程完成)
linux·chrome·bash
烟锁池塘柳05 天前
解决 Firefox 浏览器网页文本选中高亮不明显问题(含仅修改特定网站高亮显示的方案)
前端·firefox
如此这般英俊5 天前
手搓Claude Code-第十三章 background_tasks
前端·人工智能·chrome·python·算法·语言模型·自然语言处理