浏览器缓存

前言

浏览器是前端的基石,理解浏览器的工作原理对前端开发者来说十分重要。浏览器缓存是浏览器为了提高网页加载速度和减少网络请求开销等而采取的一种机制。其中涉及到数据结构、计算机网络等多方面的知识。本文将从浏览器缓存的概念、类型、工作原理以及实际应用等方面,同时也结合CDN缓存来对浏览器缓存进行详细讲解。

浏览器缓存的概念

浏览器缓存简单来说就是浏览器把用户下载过的网页资源(HTML、CSS、JS、图片等)保存在本地磁盘或内存中,当用户再次请求同一资源时,浏览器会直接从缓存中读取资源,而不是重新向服务器发送请求,从而提高网页加载速度和减少网络请求开销,也能够减轻服务器的压力。

浏览器缓存的类型

按照缓存的存储位置

按照缓存的存储位置,浏览器缓存可以分为四种类型,它们的读取优先级由高到低为:

  1. 服务工作线程缓存(Service Worker Cache)

    • 存储位置:浏览器的Service Worker缓存中。
    • 特点:可以在离线状态下访问缓存资源,生命周期较长,开发者可以通过js代码手动管理缓存,精准拦截网络请求。
    • 适用场景:适用于需要离线访问的Web应用,如PWA(Progressive Web App)。
  2. 内存缓存(Memory Cache)

    • 存储位置:电脑/手机的内存(RAM)中。
    • 特点:读取速度最快,但占用内存空间,浏览器关闭后缓存会被清空,生命周期极短。
    • 适用场景:适用于频繁访问的小型静态资源,如图片、JS文件等。
  3. 磁盘缓存(Disk Cache)

    • 存储位置:电脑/手机的硬盘中。
    • 特点:读取速度较慢,但占用空间大,浏览器关闭后缓存不会被清空,生命周期较长。
    • 适用场景:适用于大型静态资源,如视频、音频等。
  4. 推送缓存(Push Cache)

    • 存储位置:浏览器的HTTP/2推送缓存中。
    • 特点:当服务端向客户端推送资源时,浏览器会将这些资源暂时存储在推送缓存中,生命周期非常短。
    • 适用场景:用于消除关键资源的加载延迟,提升网页加载速度,但现在基本上已经被弃用。
  • 页面发起网络请求
    • 1. Service Worker 线程(最先收到 fetch 事件)
      • 分支 A:命中 Cache Storage
        • 明确直接返回数据(不经过任何 HTTP 缓存)
      • 分支 B:调用 event.respondWith(fetch(request))
        • 将请求放行,下发给浏览器底层网络层:
          • 2. HTTP Cache 模块(浏览器原生网络层)
            • 2.1 Memory Cache(内存缓存)
            • 2.2 Disk Cache(磁盘缓存)
            • 2.3 Push Cache(推送缓存)
          • 3. 真正发出网络请求(发送至 CDN / 边缘服务器 / 源站)

按照缓存的控制机制

按照缓存的控制机制,浏览器缓存可以分为两种类型:

  1. 强制缓存(Forced Cache)

    • 特点:浏览器判断缓存未过期,直接使用本地资源,完全不发请求给服务器(直接返回 200 OK (from disk/memory cache))。
    • 控制方式:通过HTTP响应头中的Cache-Control(如 max-age=3600 )和Expires字段来控制。
  2. 协商缓存(Negotiated Cache)

    • 特点:浏览器判断强缓存被禁用或者缓存已过期,向服务器发送请求,服务器根据资源是否发生变化来决定是否返回新的资源(返回 304 Not Modified 或 200 OK)。
    • 控制方式:ETag/ If-None-Match(基于内容哈希)和 Last-Modified / If-Modified-Since(基于修改时间)。

CDN缓存

CDN(Content Delivery Network,内容分发网络)是一种通过在全球范围内部署节点服务器,将用户请求就近分发到节点服务器上,从而提高访问速度和降低网络延迟的技术。CDN缓存是指CDN节点服务器对源站资源进行缓存,当用户请求资源时,CDN节点服务器会直接返回缓存的资源,而不是向源站发送请求,且缩短了请求的物理距离,从而提高访问速度和减少源站压力。

  • 1. 域名解析阶段(智能导航)

    • 用户在浏览器输入网址发起请求。
    • 智能 DNS(GSLB) 接收请求,分析用户的 IP 地址与运营商(如:上海电信)。
    • DNS 返回距离该用户物理位置最近、网络最优的 CDN 节点 IP 给浏览器。
  • 2. 边缘节点响应阶段(CDN 查找)

    • 浏览器连接该 CDN 边缘节点。
    • 分支 A:缓存命中(Cache Hit)
      • 节点发现本地存有该资源且在有效期内(未超过 s-maxage 或 CDN 缓存 TTL)。
      • 极速直接返回资源给用户(流程结束)。
    • 分支 B:缓存未命中(Cache Miss)
      • 节点本地没有该资源,或资源已过期。
      • 触发下一阶段:回源。
  • 3. 回源与数据更新阶段(拉取与缓存)

    • CDN 节点以客户端身份向**源站服务器(Origin Server)**发起请求。
    • 源站响应并返回最新资源。
    • CDN 节点保存缓存:节点将拿到的新资源存储在本地磁盘/内存中(以便后续同区域用户直接复用)。
    • CDN 节点将资源返回给用户的浏览器(流程结束)。

浏览器缓存的完整工作流程

结合上文提到的缓存类型、控制机制以及CDN缓存,我们可以总结出浏览器缓存的完整工作流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
[ 页面发起资源请求 ]
          │
          ▼
1.【Service Worker 层】(最顶层网络代理拦截)
          │
          ├───► [命中 Cache Storage] ──► 返回 200 (from ServiceWorker)
          │
          └───► [未命中 / 未注册 / SW 内显式执行 fetch()]
                                │
                                ▼
2.【HTTP 强缓存检查】(Cache-Control 优先级高于 Expires)
          │
          ├───► 检查 Cache-Control: no-store ─► [是] ─► 跳过强缓存,直接去发网络
          ├───► 检查 Cache-Control: no-cache ─► [是] ─► 跳过强缓存,强制协商缓存
          │
          ├───► [未过期] ──► 命中强缓存(依据内存/生命周期状态读取):
          │        │
          │        ├──► Memory 有缓存 (当前 Tab 运行中 / 频繁引用 / 预加载)
          │        │       └──► 返回 200 (from memory cache)
          │        │
          │        └──► Memory 已清空 (Tab关过 / 浏览器重启 / 大文件未进内存)
          │                └──► 从 Disk Cache 读取,并重新加载进 Memory
          │                        └──► 返回 200 (from disk cache)
          │
          └───► [已过期 或 显式要求协商缓存]
                                │
                                ▼
3.【构建协商缓存请求头】(装配条件 Headers)
          │
          ├───► 若存在 ETag          ──► 请求头带上 If-None-Match
          └───► 若存在 Last-Modified ──► 请求头带上 If-Modified-Since
                                │
                                ▼
4.【网络层:CDN 边缘节点校验】(Smart DNS 定位至最近 CDN 节点)
          │
          ├───► CDN 未命中 / 已过期 ──► 向源站(Origin)拉取新资源
          │
          └───► CDN 节点对比条件头:
                   ├─ 资源未改变 ──► 返回 304 Not Modified(不带 Body)
                   └─ 资源已改变 ──► 返回 200 OK(带全新资源与 Header)
                                │
                                ▼
5.【客户端接收与存储更新】
          │
          ├───► 收到 304 ──► 复用 Disk Cache,更新 HTTP Header 缓存标记
          └───► 收到 200 ──► 渲染页面,【双写/落盘】:
                   ├──► 写入 Disk Cache(持久化保存)
                   ├──► 同步载入 Memory Cache(供当前页/近期极速读取)
                   └──► 同步写入 Service Worker Cache Storage(若已启用)

Service Worker讲解

Service Worker(服务工作线程,简称 SW) 简单来说,就是运行在浏览器后台的一台“独立小型代理服务器(Proxy Server)”。 它可以拦截网页发出的网络请求,并根据开发者的逻辑决定是直接返回缓存资源,还是去网络上拉取最新资源。可以说Service Worker 的出现,赋予了前端开发者完全掌控网络请求与离线体验的能力。它是现代 PWA(Progressive Web App,渐进式 Web 应用) 技术的核心基石.

Service Worker的控制机制基本代码实现

控制机制

  1. 作用域(Scope)

    • SW 只能拦截其所在目录及子目录下的网络请求。
    • 如果脚本放在 /js/sw.js,默认作用域是 /js/,它无法拦截 /api/ 或 /index.html 的请求。
    • 最佳实践:将 sw.js 放在网站根目录下(如 /sw.js),这样就能拦截整个域名的请求。
  2. 生命周期(Lifecycle)

    • SW 的生命周期分为安装(install)、激活(activate)和运行(running)三个阶段,完全独立于网页。
    • 安装阶段:浏览器下载并安装 SW 脚本。
    • 激活阶段:SW 接管页面的控制权,可以拦截网络请求。
    • 运行阶段:SW 可以处理 fetch 事件,决定如何响应网络请求。
  3. 首次加载与更新控制

    • 首次加载不拦截:用户第一次打开页面注册 SW 时,SW 正在安装激活,默认不会拦截当次页面的请求,只有刷新或再次进入页面才会开始拦截。
      • 解决办法:在 Activate 阶段调用 clients.claim() 强制接管当前所有页面。
    • SW 脚本本身的缓存:浏览器默认也会缓存 sw.js 脚本。
      • 解决办法:服务器必须为 sw.js 设置 Cache-Control: no-cache 或 max-age=0,确保浏览器每次打开网页都能去服务器检查 SW 脚本是否有改动。

代码实现

一个完整的 Service Worker 实现分为两个文件:

  • 主线程脚本(如 main.js 或 index.html 内):负责注册与通信。
  • Service Worker 脚本(sw.js):负责生命周期控制与请求拦截。

文件 1:main.js(在页面主线程中注册 SW)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 1. 检查浏览器是否支持 Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    // 2. 注册根目录下的 sw.js,作用域默认为 '/'
    navigator.serviceWorker.register('/sw.js', { scope: '/' })
      .then((registration) => {
        console.log('Service Worker 注册成功,Scope 为:', registration.scope);
      })
      .catch((error) => {
        console.error('Service Worker 注册失败:', error);
      });
  });
}

文件 2:sw.js(核心逻辑控制)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 定义缓存版本和缓存库名称
const CACHE_NAME = 'my-app-v1';

// 预缓存的静态资源列表(App Shell)
const PRE_CACHE_RESOURCES = [
  '/',
  '/index.html',
  '/css/style.css',
  '/js/main.js',
  '/images/logo.png'
];

// ==========================================
// 1. Install 事件:预缓存核心资源
// ==========================================
self.addEventListener('install', (event) => {
  console.log('[SW] 正在安装...');
  
  // event.waitUntil 确保 SW 在缓存完成前不会变为 active 状态
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('[SW] 正在预缓存静态资源');
      return cache.addAll(PRE_CACHE_RESOURCES);
    }).then(() => {
      // 强制跳过等待期(skipWaiting),让新 SW 安装后立即激活,不用等旧页面关闭
      return self.skipWaiting();
    })
  );
});

// ==========================================
// 2. Activate 事件:清理旧缓存与接管页面
// ==========================================
self.addEventListener('activate', (event) => {
  console.log('[SW] 正在激活...');

  event.waitUntil(
    // 获取所有的缓存 key
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cache) => {
          // 如果不是当前版本的缓存,全部删掉
          if (cache !== CACHE_NAME) {
            console.log('[SW] 删除旧版本缓存:', cache);
            return caches.delete(cache);
          }
        })
      );
    }).then(() => {
      // 强制让 SW 立即接管当前所有打开的页面(Clients)
      return self.clients.claim();
    })
  );
});

// ==========================================
// 3. Fetch 事件:拦截请求(策略:Network First / 优先网络)
// ==========================================
self.addEventListener('fetch', (event) => {
  // 只拦截 GET 请求,Post/Put 等请求不缓存
  if (event.request.method !== 'GET') return;

  event.respondWith(
    // 先尝试发网络请求
    fetch(event.request)
      .then((networkResponse) => {
        // 如果网络正常,且响应有效,克隆一份响应存进 Cache Storage
        if (networkResponse && networkResponse.status === 200) {
          const responseToCache = networkResponse.clone();
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, responseToCache);
          });
        }
        return networkResponse;
      })
      .catch(() => {
        // 网络失败(断网/服务器挂了),降级从 Cache Storage 里找缓存
        console.log('[SW] 网络离线,尝试从 Cache 读取:', event.request.url);
        return caches.match(event.request);
      })
  );
});

几种常见的 SW 缓存拦截策略

Cache First(优先缓存,适合静态不常变的文件如图片、字体)

先查 SW 缓存,如果有直接返回;没有再发网络请求,成功后写入缓存。

1
2
3
caches.match(event.request).then((cachedResponse) => {
  return cachedResponse || fetch(event.request);
});

2. Network First(优先网络,适合实时数据/经常更新的数据)

先发网络请求;如果网络通畅就直接用,并更新缓存;网络断开时(Offline)降级读取缓存。

3. Stale-While-Revalidate(缓存优先,后台静默更新,适合新闻/博客文章)

立即返回本地缓存给用户看,同时后台偷偷发网络请求拉取新数据并更新缓存,下一次刷新就能看到最新的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
caches.open(CACHE_NAME).then((cache) => {
  return cache.match(event.request).then((cachedResponse) => {
    // 后台异步去发网络请求更新缓存
    const fetchPromise = fetch(event.request).then((networkResponse) => {
      cache.put(event.request, networkResponse.clone());
      return networkResponse;
    });
    // 如果有本地缓存优先用本地,没有就等网络请求
    return cachedResponse || fetchPromise;
  });
});

Service Worker 生产环境的最佳实践建议

  1. 必须在 HTTPS 下运行:SW 拥有拦截所有请求的能力,为了安全,浏览器限制 SW 只能在 HTTPS 环境或 http://localhost 下生效。

  2. 不要原生手写复杂 SW:原生手写逻辑容易造成缓存死锁(页面永远刷不出新代码)。推荐直接使用 Google 官方的 Workbox 库(Vite/Webpack 均有对应插件 vite-plugin-pwa),它把常用的缓存策略和版本更新封装得非常安全。

强缓存与协商缓存控制细节

强缓存和协商缓存是浏览器缓存的两种主要控制机制,都是通过HTTP请求头和响应头来实现的。下面我们详细讲解它们的控制细节。

强缓存(Strong Cache)

强缓存无需发送网络请求,浏览器直接检查本地存储(Memory Cache / Disk Cache)。如果命中且未过期,直接返回 200 OK (from memory/disk cache)。


1. 核心控制头对比

字段 类型 版本 示例 / 取值 底层细节与行为
Cache-Control 响应头 HTTP/1.1 max-age=31536000 绝对优先。相对时间(秒)。指定资源在客户端被视为新鲜(Fresh)的最大有效时长。
Expires 响应头 HTTP/1.0 Expires: Sun, 13 Sep 2026 08:00:00 GMT 绝对时间戳。受限于客户端本地电脑时钟(若用户修改系统时间,缓存判定会失效)。当与 max-age 共存时被覆盖。

2. Response Headers(响应头)中的 Cache-Control 指令

指令 示例 说明与适用场景
max-age= max-age=3600 资源强缓存有效时间(秒)。表示指定时间内无需重新发请求。
s-maxage= s-maxage=86400 仅对共享缓存(如 CDN / 代理服务器)生效,优先级高于 max-age。
no-cache no-cache 跳过强缓存,强制走协商缓存。表明可以存储该资源,但使用前必须向服务器校验(携带 ETag / Last-Modified)。
no-store no-store 绝对禁止任何缓存。浏览器和 CDN 不得存储响应副本,每次必须全量下载。
public public 表明该响应可以被任何中继节点(客户端、CDN、代理服务器)缓存。
private private 表明该响应为单用户私有(如含个人信息的 API),禁止 CDN 节点存储,仅允许浏览器缓存。
immutable immutable 告知浏览器资源在生命周期内绝不改变(常用于 Hash 命名静态文件)。刷新页面也不发协商请求。
stale-while-revalidate= stale-while-revalidate=60 允许先展示已过期的旧缓存,同时后台静默发请求拉取最新资源更新缓存。

协商缓存(Validation Cache)

当强缓存失效(已过期)或显式设置为 no-cache 时,浏览器携带校验头向服务器发请求:

  • 资源未变动:返回 304 Not Modified(无响应体 Body),通知浏览器复用本地缓存。
  • 资源已变动:返回 200 OK(带全新 Body 与最新 Header),浏览器使用新资源并更新本地缓存。

1. 协商缓存成对校验头

校验维度 响应头(服务器返回并保存) 请求头(客户端协商时携带) 校验原理与精确度
内容 Hash 校验 (优先选用) ETag
ETag: "w/33a64df5"
If-None-Match
If-None-Match: "w/33a64df5"
基于文件内容或元信息生成的指纹。
• 强 ETag:字节级变动全更新。
• 弱 ETag(W/ 前缀):仅语义变动时更新。
修改时间校验 (降级备用) Last-Modified
Last-Modified: Sun, 13 Sep 2026 08:00:00 GMT
If-Modified-Since
If-Modified-Since: Sun, 13 Sep 2026 08:00:00 GMT
基于文件在磁盘上的最后修改时间戳(精确到秒)。

2. ETag 与 Last-Modified 深度对比

  1. 精确度:Last-Modified 只能精确到秒,1 秒内的多次修改无法识别;ETag 基于内容 Hash,哪怕改动 1 个字符也能精确感知。
  2. 服务器重写场景:仅编辑文件但内容未变时,Last-Modified 时间戳更新会导致误判;ETag 校验 Hash 一致依然能准确命中 304。
  3. 计算成本:Last-Modified 直接读取文件系统的 stat 元数据,极快;ETag 计算大文件 Hash 会产生微量 CPU 开销。

Request Headers(请求头)中的 Cache-Control 指令

客户端(浏览器、Fetch/Axios 或用户刷新行为)发送请求时,通过请求头中的 Cache-Control 表达对响应内容的新鲜度期望:

指令 示例 说明与适用场景
no-cache Cache-Control: no-cache 告知中继与源站:跳过强缓存,必须向源站进行协商校验(按 F5 刷新时浏览器自动发送)。
no-store Cache-Control: no-store 告知所有节点:不要使用任何本地缓存,也不要存储本次响应(按 Ctrl + F5 强制刷新时发送)。
max-age= Cache-Control: max-age=0 告知服务器:不接受过期时间超过 N 秒的资源。设为 0 表示强制发起校验请求。
max-stale[=] Cache-Control: max-stale=3600 告知服务器:可以接受已经过期的资源,只要过期时间不超过 N 秒(弱网/断网降级时使用)。
min-fresh= Cache-Control: min-fresh=60 告知服务器:要求返回的资源在未来 N 秒内依然是新鲜(未过期)的。
only-if-cached Cache-Control: only-if-cached 告知客户端/代理:只从本地/代理缓存中获取。若无缓存直接返回 504 Gateway Timeout(离线模式常用)。

现代 Web 工程最佳配置实践

  1. 静态资源(JS/CSS/图片):

    • 使用 Hash 命名(如 main.abc123.js),并设置:
      1
      
      Cache-Control: public, max-age=31536000, immutable
      
    • 这样浏览器可以长期缓存,且不会发协商请求。
  2. 动态资源(API 接口):

    • 设置:
      1
      
      Cache-Control: no-cache
      
    • 这样浏览器每次请求都会发协商请求,确保获取最新数据
  3. 敏感资源(用户隐私数据):

    • 设置:
      1
      
      Cache-Control: private, no-store
      
    • 禁止 CDN 或代理缓存,确保数据安全。

结语

浏览器缓存是前端性能优化的重要手段,合理利用强缓存和协商缓存可以显著提升网页加载速度,减少服务器压力。通过理解浏览器缓存的工作原理、类型以及控制机制,前端开发者可以更好地设计和优化网页资源的缓存策略。同时,结合CDN缓存的使用,可以进一步提升用户体验和网站性能。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 Stack 由 Jimmy 设计