跳到主要内容

浏览器缓存机制

问题

请介绍浏览器的缓存机制,包括强缓存、协商缓存的工作原理和区别。

答案

浏览器缓存是提升网页性能的重要手段,可以减少网络请求、降低服务器压力、加快页面加载速度。浏览器缓存主要分为强缓存协商缓存两种策略。

强缓存(Strong Cache)

强缓存指浏览器直接从本地缓存读取资源,不向服务器发送任何请求

Expires(HTTP/1.0)

Expires 是一个绝对时间,表示资源的过期时间。

Expires: Thu, 12 Feb 2027 08:00:00 GMT
Expires 的问题
  • 依赖客户端时间:如果用户修改本地时间,可能导致缓存失效
  • 精度问题:只能精确到秒
  • 已被 Cache-Control 取代:HTTP/1.1 推荐使用 Cache-Control

Cache-Control(HTTP/1.1)

Cache-Control 使用相对时间,是现代浏览器缓存的主要控制方式。

Cache-Control: max-age=31536000, public

常用指令

指令说明示例
max-age=<seconds>资源最大有效时间(秒)max-age=3600(1小时)
s-maxage=<seconds>CDN/代理服务器缓存时间s-maxage=86400
public可被任何缓存存储(包括 CDN)public, max-age=3600
private只能被浏览器缓存,不能被 CDN 缓存private, max-age=600
no-cache使用缓存前必须向服务器验证no-cache
no-store完全禁止缓存no-store
must-revalidate过期后必须向服务器验证max-age=3600, must-revalidate
immutable资源永不改变,不需要验证max-age=31536000, immutable
Cache-Control vs Expires

Cache-ControlExpires 同时存在时,Cache-Control 优先级更高

强缓存响应示例

// Express.js 设置强缓存
import express from 'express';

const app = express();

// 静态资源设置长期缓存(1年)
app.use('/static', express.static('public', {
maxAge: 31536000000, // 毫秒
immutable: true
}));

// 或手动设置响应头
app.get('/api/config', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1小时
res.json({ version: '1.0.0' });
});

强缓存状态码

状态说明
200 (from memory cache)从内存缓存读取,页面未关闭时
200 (from disk cache)从磁盘缓存读取,页面关闭后重新打开

协商缓存(Negotiation Cache)

协商缓存需要向服务器发送请求,由服务器判断资源是否更新。如果未更新,返回 304 Not Modified,浏览器使用本地缓存。

Last-Modified / If-Modified-Since

基于文件最后修改时间判断资源是否更新。

首次请求:服务器返回 Last-Modified

HTTP/1.1 200 OK
Last-Modified: Wed, 11 Feb 2026 10:00:00 GMT
Content-Type: text/html

再次请求:浏览器携带 If-Modified-Since

GET /index.html HTTP/1.1
If-Modified-Since: Wed, 11 Feb 2026 10:00:00 GMT

服务器响应

  • 未修改:返回 304 Not Modified(无响应体)
  • 已修改:返回 200 OK + 新资源
Last-Modified 的局限性
  1. 精度问题:只能精确到秒,1 秒内多次修改无法检测
  2. 内容未变但时间变了:如重新保存文件,时间改变但内容未变
  3. 负载均衡问题:多台服务器的文件修改时间可能不一致

ETag / If-None-Match

基于资源内容的唯一标识符(哈希值)判断资源是否更新,解决了 Last-Modified 的局限性。

首次请求:服务器返回 ETag

HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: text/html

再次请求:浏览器携带 If-None-Match

GET /index.html HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

服务器响应

  • ETag 匹配:返回 304 Not Modified
  • ETag 不匹配:返回 200 OK + 新资源

ETag 的类型

类型格式说明
强 ETag"abc123"内容完全一致才匹配
弱 ETagW/"abc123"语义相同即可匹配(如忽略空格差异)
ETag vs Last-Modified
  • ETag 优先级更高:同时存在时,服务器优先验证 ETag
  • ETag 更精确:基于内容哈希,不受时间影响
  • ETag 开销更大:需要计算哈希值,增加服务器负担

协商缓存实现示例

import express, { Request, Response } from 'express';
import crypto from 'crypto';
import fs from 'fs';

const app = express();

// 使用 ETag 实现协商缓存
app.get('/data.json', (req: Request, res: Response) => {
const data = JSON.stringify({ message: 'Hello World', timestamp: Date.now() });
const etag = crypto.createHash('md5').update(data).digest('hex');

// 设置 ETag
res.setHeader('ETag', `"${etag}"`);
res.setHeader('Cache-Control', 'no-cache'); // 每次都验证

// 检查 If-None-Match
const clientEtag = req.headers['if-none-match'];
if (clientEtag === `"${etag}"`) {
res.status(304).end(); // 资源未变化
return;
}

res.json(JSON.parse(data));
});

// 使用 Last-Modified 实现协商缓存
app.get('/file/:name', (req: Request, res: Response) => {
const filePath = `./files/${req.params.name}`;
const stat = fs.statSync(filePath);
const lastModified = stat.mtime.toUTCString();

res.setHeader('Last-Modified', lastModified);
res.setHeader('Cache-Control', 'no-cache');

const clientModified = req.headers['if-modified-since'];
if (clientModified === lastModified) {
res.status(304).end();
return;
}

res.sendFile(filePath);
});

缓存策略对比

特性强缓存协商缓存
是否发送请求
HTTP 状态码200 (from cache)304 Not Modified
响应头Cache-Control, ExpiresETag, Last-Modified
请求头If-None-Match, If-Modified-Since
性能最优(无网络请求)较优(只传输响应头)
适用场景不常变化的静态资源需要验证更新的资源

缓存决策流程

实际应用:缓存策略配置

不同资源的缓存策略

// Nginx 配置示例转换为 Node.js
import express from 'express';

const app = express();

// HTML 文件:不缓存或协商缓存
app.get('*.html', (req, res, next) => {
res.setHeader('Cache-Control', 'no-cache');
next();
});

// JS/CSS 带 hash 的文件:长期强缓存
app.get(/\.(js|css)$/, (req, res, next) => {
if (req.url.includes('.') && /\.[a-f0-9]{8}\./.test(req.url)) {
// 带 hash 的文件,如 app.a1b2c3d4.js
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else {
// 不带 hash 的文件
res.setHeader('Cache-Control', 'no-cache');
}
next();
});

// 图片/字体:长期缓存
app.get(/\.(png|jpg|gif|svg|woff2?)$/, (req, res, next) => {
res.setHeader('Cache-Control', 'public, max-age=31536000');
next();
});

// API 响应:不缓存
app.get('/api/*', (req, res, next) => {
res.setHeader('Cache-Control', 'no-store');
next();
});

推荐的缓存策略

资源类型缓存策略Cache-Control 值
HTML 入口协商缓存no-cache
JS/CSS(带 hash)强缓存 1 年max-age=31536000, immutable
JS/CSS(不带 hash)协商缓存no-cache
图片/字体/视频强缓存 1 年max-age=31536000
API 响应不缓存no-store
用户敏感数据不缓存private, no-store

前端工程化中的缓存

现代前端构建工具(如 WebpackVite)通过内容哈希实现最佳缓存策略:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
build: {
// 生成带 hash 的文件名
rollupOptions: {
output: {
// 入口文件
entryFileNames: 'assets/[name].[hash].js',
// 代码分割的 chunk
chunkFileNames: 'assets/[name].[hash].js',
// 静态资源
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
});
// 构建输出示例
dist/
├── index.html # 不带 hash,协商缓存
├── assets/
│ ├── index.a1b2c3d4.js # 带 hash,强缓存
│ ├── vendor.e5f6g7h8.js # 带 hash,强缓存
│ └── style.i9j0k1l2.css # 带 hash,强缓存
内容哈希的优势
  • 内容变化 → hash 变化 → URL 变化:浏览器自动请求新资源
  • 内容不变 → hash 不变 → URL 不变:继续使用强缓存
  • 实现精准的缓存失效:只更新变化的文件

缓存位置

浏览器缓存资源的位置有多种,按查找优先级从高到低排序:

缓存位置说明特点生命周期
Service Worker Cache开发者可编程控制优先级最高,支持离线手动管理
Memory Cache内存缓存速度最快,容量小页面关闭即清除
Disk Cache磁盘缓存容量大,速度较慢根据策略清理
Push CacheHTTP/2 推送缓存会话级别约 5 分钟

1. Service Worker Cache

Service Worker 是运行在浏览器背后的独立线程,可以拦截网络请求并返回缓存资源。

特点

  • 优先级最高:在所有其他缓存之前检查
  • 完全可控:开发者决定缓存什么、何时更新
  • 支持离线:即使断网也能返回缓存资源
  • 独立生命周期:不随页面关闭而销毁

常见缓存策略

// sw.ts - Service Worker 文件

// 1. Cache First(缓存优先)- 适合静态资源
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) {
return cached;
}
const response = await fetch(request);
const cache = await caches.open('static-v1');
cache.put(request, response.clone());
return response;
}

// 2. Network First(网络优先)- 适合 API 请求
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
const cache = await caches.open('api-v1');
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
if (cached) {
return cached;
}
throw new Error('No cached data available');
}
}

// 3. Stale While Revalidate(返回缓存同时更新)
async function staleWhileRevalidate(request: Request): Promise<Response> {
const cache = await caches.open('dynamic-v1');
const cached = await cache.match(request);

// 后台更新缓存
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});

// 优先返回缓存,没有则等待网络
return cached || fetchPromise;
}

// 注册事件监听
self.addEventListener('fetch', (event: FetchEvent) => {
const url = new URL(event.request.url);

if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
} else if (url.pathname.match(/\.(js|css|png|jpg)$/)) {
event.respondWith(cacheFirst(event.request));
} else {
event.respondWith(staleWhileRevalidate(event.request));
}
});

2. Memory Cache(内存缓存)

Memory Cache 将资源直接存储在内存中,读取速度最快。

特点

  • 速度极快:直接从 RAM 读取,无磁盘 I/O
  • 容量有限:受可用内存限制
  • 生命周期短:标签页关闭即清除
  • 自动管理:浏览器自动决定缓存内容

存储优先级(浏览器倾向于将以下资源放入内存缓存):

资源类型Memory Cache 概率原因
preload 预加载资源很快会被使用
小型 JS/CSS 文件体积小,频繁使用
base64 图片已在内存中
大型图片/视频占用内存过大
// 使用 preload 提示浏览器预加载到内存
// HTML 中
// <link rel="preload" href="critical.js" as="script">
// <link rel="preload" href="hero.jpg" as="image">

// 检查资源是否来自 Memory Cache(开发者工具)
// Network 面板 → Size 列显示 "(memory cache)"
Memory Cache vs Disk Cache
  • 刷新页面:优先使用 Memory Cache
  • 关闭后重新打开:只能使用 Disk Cache
  • 相同资源多个标签页:可能共享 Memory Cache

3. Disk Cache(磁盘缓存)

Disk Cache 将资源存储在硬盘上,容量大且持久化。

特点

  • 容量大:可存储大量资源(通常数百 MB 到数 GB)
  • 持久化:关闭浏览器后仍然存在
  • 速度较慢:需要磁盘 I/O
  • 遵循 HTTP 缓存策略:根据 Cache-Control 等头部决定缓存时长

存储内容

资源类型说明
HTML 文件协商缓存时存储
JS/CSS 文件强缓存的静态资源
图片/字体大型媒体资源
其他静态资源根据响应头决定
// 浏览器决定存储位置的简化逻辑
function decideCacheLocation(resource: Resource): 'memory' | 'disk' {
// 1. 正在使用的资源倾向于内存
if (resource.isCurrentlyUsed) {
return 'memory';
}

// 2. 大文件倾向于磁盘
if (resource.size > 1024 * 1024) { // > 1MB
return 'disk';
}

// 3. 预加载资源倾向于内存
if (resource.isPreloaded) {
return 'memory';
}

// 4. 其他情况通常存磁盘
return 'disk';
}

查看 Disk Cache

# Chrome 缓存位置(macOS)
~/Library/Caches/Google/Chrome/Default/Cache

# Chrome 缓存位置(Windows)
%LOCALAPPDATA%\Google\Chrome\User Data\Default\Cache

4. Push Cache(推送缓存)

Push Cache 是 HTTP/2 Server Push 的缓存,是最后一道防线。

特点

  • 生命周期极短:只存在于会话(Session)中,约 5 分钟
  • 一次性使用:被使用后即从 Push Cache 中移除
  • 优先级最低:只有其他缓存都未命中时才检查
  • HTTP/2 专属:只有 HTTP/2 连接才有
// 服务器推送示例(Node.js with http2)
import http2 from 'http2';
import fs from 'fs';

const server = http2.createSecureServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt')
});

server.on('stream', (stream, headers) => {
const path = headers[':path'];

if (path === '/index.html') {
// 推送关联资源
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
if (!err) {
pushStream.respond({ ':status': 200, 'content-type': 'text/css' });
pushStream.end(fs.readFileSync('style.css'));
}
});

stream.pushStream({ ':path': '/app.js' }, (err, pushStream) => {
if (!err) {
pushStream.respond({ ':status': 200, 'content-type': 'application/javascript' });
pushStream.end(fs.readFileSync('app.js'));
}
});

// 返回 HTML
stream.respond({ ':status': 200, 'content-type': 'text/html' });
stream.end(fs.readFileSync('index.html'));
}
});

server.listen(443);
Push Cache 的限制
  • 不同页面/标签页无法共享(不同会话)
  • 浏览器可能拒绝推送(如已有缓存)
  • 实际使用较少,HTTP/3 弃用了 Server Push

缓存位置总结

对比项Service WorkerMemory CacheDisk CachePush Cache
控制方式开发者编程浏览器自动浏览器自动服务器推送
读取速度最快较慢
容量配额限制
生命周期手动管理页面级持久化会话级(~5分钟)
离线支持
跨页面共享✅ 同源部分

缓存问题与解决方案

问题 1:缓存更新不及时

症状:用户看到旧版本页面

解决方案

// 1. HTML 使用协商缓存
// Cache-Control: no-cache

// 2. 资源文件使用内容哈希
// app.a1b2c3d4.js → app.e5f6g7h8.js

// 3. 版本号查询参数(不推荐)
// app.js?v=1.0.1 → app.js?v=1.0.2

问题 2:强制刷新缓存

// 用户操作
// - 普通刷新:F5 / Cmd+R → 使用缓存
// - 强制刷新:Ctrl+F5 / Cmd+Shift+R → 跳过缓存
// - 清空缓存:DevTools → Application → Clear storage

// 代码中强制获取最新资源
fetch('/api/data', {
cache: 'no-store' // 完全跳过缓存
});

fetch('/api/data', {
cache: 'reload' // 忽略缓存,但会更新缓存
});

问题 3:CDN 缓存更新

// CDN 缓存刷新策略
// 1. 使用内容哈希(推荐)
// 2. 手动刷新 CDN 缓存
// 3. 设置合适的 s-maxage

// Nginx 配置示例
/*
location /static/ {
add_header Cache-Control "public, max-age=31536000, s-maxage=86400";
# 浏览器缓存 1 年,CDN 缓存 1 天
}
*/

常见面试问题

Q1: 强缓存和协商缓存的区别是什么?

答案

对比项强缓存协商缓存
是否发请求否,直接使用本地缓存是,向服务器验证
状态码200 (from cache)304 Not Modified
控制字段Cache-Control, ExpiresETag, Last-Modified
性能最优(0 网络请求)较优(只传输头部)
使用场景不常变化的静态资源需要验证更新的资源

工作流程

// 伪代码:浏览器缓存判断逻辑
function handleRequest(url: string): Response {
const cache = getLocalCache(url);

if (!cache) {
return fetchFromServer(url); // 无缓存,请求服务器
}

// 1. 检查强缓存
if (cache.cacheControl && !isExpired(cache.cacheControl.maxAge)) {
return cache.response; // 强缓存命中,直接返回
}

// 2. 强缓存失效,进入协商缓存
const headers: Record<string, string> = {};
if (cache.etag) {
headers['If-None-Match'] = cache.etag;
}
if (cache.lastModified) {
headers['If-Modified-Since'] = cache.lastModified;
}

const response = fetchFromServer(url, { headers });

if (response.status === 304) {
return cache.response; // 协商缓存命中
}

return response; // 返回新资源
}

Q2: Cache-Control 的 no-cache 和 no-store 有什么区别?

答案

指令含义是否缓存使用场景
no-cache使用缓存前必须向服务器验证✅ 缓存HTML 入口文件
no-store完全禁止缓存❌ 不缓存敏感数据(银行页面)
// no-cache:每次都要验证,但会存储缓存
// 适用于需要确保获取最新版本,但可以利用 304 优化的场景
res.setHeader('Cache-Control', 'no-cache');

// no-store:完全不缓存,每次都重新下载
// 适用于敏感数据,如银行账户信息
res.setHeader('Cache-Control', 'no-store');

// 常见误解
// ❌ 错误理解:no-cache 表示不缓存
// ✅ 正确理解:no-cache 表示缓存但需验证

流程对比

Q3: 如何设计一个最优的前端缓存策略?

答案

核心原则

  1. HTML 文件:使用协商缓存(no-cache),确保入口始终最新
  2. 静态资源:使用内容哈希 + 长期强缓存(immutable
  3. API 响应:根据业务需求设置,敏感数据使用 no-store

完整配置示例

// server.ts - Express 缓存配置
import express from 'express';
import path from 'path';

const app = express();

// 设置不同资源的缓存策略
app.use((req, res, next) => {
const url = req.url;

if (url.endsWith('.html') || url === '/') {
// HTML:协商缓存,确保获取最新版本
res.setHeader('Cache-Control', 'no-cache');
} else if (/\.[a-f0-9]{8,}\.(js|css)$/.test(url)) {
// 带 hash 的 JS/CSS:强缓存 1 年 + immutable
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else if (/\.(js|css)$/.test(url)) {
// 不带 hash 的 JS/CSS:协商缓存
res.setHeader('Cache-Control', 'no-cache');
} else if (/\.(png|jpg|gif|svg|ico|woff2?|ttf|eot)$/.test(url)) {
// 图片和字体:强缓存 1 年
res.setHeader('Cache-Control', 'public, max-age=31536000');
}

next();
});

// API 路由
app.use('/api', (req, res, next) => {
// API 默认不缓存
res.setHeader('Cache-Control', 'no-store');
next();
});

app.use(express.static('dist'));

Nginx 配置参考

# HTML 文件
location ~* \.html$ {
add_header Cache-Control "no-cache";
}

# 带 hash 的静态资源
location ~* \.[a-f0-9]{8}\.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# 图片和字体
location ~* \.(png|jpg|gif|svg|woff2?)$ {
add_header Cache-Control "public, max-age=31536000";
}

# API
location /api/ {
add_header Cache-Control "no-store";
}

Q4: ETag 和 Last-Modified 哪个更好?应该如何选择?

答案

对比分析

特性ETagLast-Modified
精度高(基于内容哈希)低(秒级时间戳)
性能开销高(需计算哈希)低(直接读取修改时间)
分布式一致性好(内容相同则 ETag 相同)差(多服务器时间可能不同)
浏览器支持HTTP/1.1+HTTP/1.0+
优先级

选择建议

// 推荐:同时使用两者
// ETag 作为主要验证,Last-Modified 作为兜底

app.get('/resource', (req, res) => {
const content = getResourceContent();
const stat = getResourceStat();

// 同时设置两者
res.setHeader('ETag', `"${calculateHash(content)}"`);
res.setHeader('Last-Modified', stat.mtime.toUTCString());

// 优先检查 ETag
const clientEtag = req.headers['if-none-match'];
const clientModified = req.headers['if-modified-since'];

if (clientEtag && clientEtag === res.getHeader('ETag')) {
return res.status(304).end();
}

if (clientModified && clientModified === res.getHeader('Last-Modified')) {
return res.status(304).end();
}

res.send(content);
});

使用场景

  • 优先 ETag:内容频繁变化、需要精确验证、分布式环境
  • 优先 Last-Modified:大文件、计算哈希开销大、简单场景
  • 两者结合:通用场景,提供最佳兼容性

Q5: 实际项目中如何设计缓存策略?(HTML no-cache + 资源文件 hash 长缓存)

答案

现代前端项目的缓存策略遵循一个核心原则:HTML 入口文件使用协商缓存,静态资源文件通过内容哈希实现长期强缓存。这种策略能在保证用户始终获取最新版本的同时,最大化利用缓存提升加载速度。

核心思路

分层缓存策略

资源类型缓存策略Cache-Control原因
index.html协商缓存no-cache入口必须最新,才能引用到正确的哈希资源
app.a1b2c3d4.js强缓存 1 年max-age=31536000, immutable内容变则 hash 变,URL 自然更新
style.e5f6g7h8.css强缓存 1 年max-age=31536000, immutable同上
logo.png强缓存 1 年max-age=31536000图片通常不频繁更新
/api/*不缓存no-storeAPI 数据实时性要求高
用户隐私数据不缓存private, no-store安全要求,不允许 CDN 缓存

完整的构建 + 部署配置

vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
build: {
rollupOptions: {
output: {
// 内容哈希:内容变 → hash 变 → URL 变 → 缓存自动失效
entryFileNames: 'assets/js/[name].[hash].js',
chunkFileNames: 'assets/js/[name].[hash].js',
assetFileNames: 'assets/[ext]/[name].[hash].[ext]',

// 合理分包:第三方库单独打包,变化频率低,缓存命中率高
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['antd'],
},
},
},
},
});
nginx.conf
server {
listen 80;
root /usr/share/nginx/html;

# HTML 入口:协商缓存,每次验证
location / {
try_files $uri /index.html;
add_header Cache-Control "no-cache";
}

# 带 hash 的静态资源:强缓存 1 年 + immutable
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# 图片/字体
location ~* \.(png|jpg|gif|svg|ico|woff2?|ttf)$ {
add_header Cache-Control "public, max-age=31536000";
}

# API 代理
location /api/ {
proxy_pass http://backend;
add_header Cache-Control "no-store";
}
}

为什么这个策略有效?

发版前:
index.html → <script src="/assets/js/app.a1b2c3d4.js">
^^^^^^^^
旧 hash

发版后:
index.html → <script src="/assets/js/app.e5f6g7h8.js">
^^^^^^^^
新 hash(内容变了)

用户访问 →
1. 请求 index.html → no-cache → 服务器返回新 HTML(或 304)
2. 解析 HTML → 发现引用了 app.e5f6g7h8.js(新 URL)
3. 本地无此 URL 的缓存 → 请求服务器下载新文件
4. 旧的 app.a1b2c3d4.js 自然过期,不再被引用
分包策略的缓存优势

将第三方库(如 React、Lodash)单独打包为 vendor.[hash].js。因为第三方库版本不常变,所以其 hash 长期不变,用户只需下载业务代码的变更,vendor 文件始终命中缓存

Q6: Service Worker 缓存和 HTTP 缓存的关系

答案

Service Worker 缓存和 HTTP 缓存是两套独立的缓存机制,但在浏览器请求资源时有明确的优先级关系。Service Worker 缓存的优先级高于 HTTP 缓存。

请求经过的缓存层级

核心区别

对比项Service Worker 缓存HTTP 缓存
控制方开发者完全控制浏览器根据响应头自动管理
优先级最高较低(SW 之后)
缓存策略代码中自定义(Cache First / Network First 等)Cache-Control / ETag / Last-Modified
离线支持支持(核心优势)不支持
生命周期手动管理(caches.delete根据 HTTP 头部自动管理
作用范围同源下所有页面单个资源
更新机制SW 文件变化 → 安装新 SW → activate 时清理旧缓存过期后自动协商

两者配合的最佳实践

sw.ts - Service Worker 缓存策略
const CACHE_NAME = 'app-v2';
const STATIC_ASSETS = ['/index.html', '/offline.html'];

// 安装阶段:预缓存关键资源
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
);
});

// 激活阶段:清理旧版本缓存
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
)
);
});

// 请求拦截:根据资源类型选择策略
self.addEventListener('fetch', (event: FetchEvent) => {
const url = new URL(event.request.url);

if (url.pathname.startsWith('/api/')) {
// API 请求:Network First(优先网络,失败用缓存)
event.respondWith(networkFirst(event.request));
} else if (url.pathname.match(/\.[a-f0-9]{8}\.(js|css)$/)) {
// 带 hash 的静态资源:Cache First(优先缓存)
// 因为 HTTP 层已经有 immutable 强缓存,SW 做双重保障
event.respondWith(cacheFirst(event.request));
} else if (url.pathname.endsWith('.html') || url.pathname === '/') {
// HTML 入口:Network First(确保最新)
event.respondWith(networkFirstWithFallback(event.request));
}
});

async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
return cached || fetch(request);
}

async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch {
return (await caches.match(request)) || new Response('Offline', { status: 503 });
}
}

async function networkFirstWithFallback(request: Request): Promise<Response> {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
return cached || caches.match('/offline.html') as Promise<Response>;
}
}
Service Worker 缓存的陷阱

SW 可能绕过 HTTP 缓存:如果 SW 使用 Cache First 策略缓存了 HTML,那么即使服务器已更新,用户仍会看到旧版本。因此 HTML 在 SW 中必须使用 Network First 策略

// ❌ 错误:HTML 使用 Cache First,会导致永远无法更新
event.respondWith(cacheFirst(event.request)); // 对 HTML 不要这样做

// ✅ 正确:HTML 使用 Network First
event.respondWith(networkFirst(event.request));

SW 缓存与 HTTP 缓存的交互关系

当 Service Worker 调用 fetch() 请求资源时,这个请求仍然会经过 HTTP 缓存层

Q7: CDN 缓存和浏览器缓存如何配合?

答案

CDN(内容分发网络)缓存和浏览器缓存是两级缓存的关系。CDN 缓存在服务端边缘节点,浏览器缓存在客户端本地。两者通过 HTTP 响应头协调工作。

请求链路

CDN 缓存 vs 浏览器缓存

对比项CDN 缓存浏览器缓存
缓存位置CDN 边缘节点(服务端)用户设备(客户端)
服务对象所有用户共享单个用户私有
控制字段s-maxageCache-Control: publicmax-ageCache-Control: private
更新方式主动刷新 CDN 缓存(Purge)等待过期或强制刷新
网络请求需要请求,但距离近、延迟低不需要网络请求(强缓存时)
安全性不能缓存含用户隐私的内容可以缓存私有内容

Cache-Control 中 CDN 相关指令

# s-maxage:CDN 专用缓存时间(覆盖 max-age)
Cache-Control: public, max-age=3600, s-maxage=86400
# 浏览器缓存 1 小时,CDN 缓存 1 天

# public:允许 CDN 缓存
Cache-Control: public, max-age=31536000
# CDN 和浏览器都可以缓存

# private:禁止 CDN 缓存
Cache-Control: private, max-age=3600
# 只有浏览器可以缓存(如:含用户信息的页面)

分层缓存策略配置

server.ts - 针对不同资源配置 CDN + 浏览器缓存
import express from 'express';

const app = express();

app.use((req, res, next) => {
const url = req.url;

if (url.endsWith('.html') || url === '/') {
// HTML:浏览器不缓存,CDN 短期缓存(方便回源刷新)
res.setHeader('Cache-Control', 'no-cache, s-maxage=60');
// CDN 缓存 60 秒,发版时主动 Purge CDN
} else if (/\.[a-f0-9]{8}\.(js|css)$/.test(url)) {
// 带 hash 的资源:浏览器和 CDN 都长期缓存
res.setHeader('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable');
} else if (/\.(png|jpg|gif|svg|woff2?)$/.test(url)) {
// 图片字体:浏览器长期缓存,CDN 缓存 7 天
res.setHeader('Cache-Control', 'public, max-age=31536000, s-maxage=604800');
} else if (url.startsWith('/api/')) {
// API:都不缓存
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('CDN-Cache-Control', 'no-store');
}

next();
});

CDN 缓存刷新(Purge)的时机

deploy.ts - 部署时刷新 CDN 缓存
interface CDNPurgeOptions {
urls?: string[];
paths?: string[];
all?: boolean;
}

async function purgeAfterDeploy(): Promise<void> {
// 1. 部署新版本后,刷新 HTML 文件的 CDN 缓存
await purgeCDNCache({
urls: [
'https://www.example.com/',
'https://www.example.com/index.html',
],
});
// 2. 带 hash 的资源无需刷新(新 URL = 新资源,旧 URL 自然过期)
console.log('CDN 缓存刷新完成');
}

async function purgeCDNCache(options: CDNPurgeOptions): Promise<void> {
// 调用 CDN 服务商 API(如阿里云、Cloudflare、AWS CloudFront)
const response = await fetch('https://cdn-api.example.com/purge', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.CDN_TOKEN}` },
body: JSON.stringify(options),
});

if (!response.ok) {
throw new Error(`CDN 刷新失败: ${response.statusText}`);
}
}
CDN + 浏览器缓存最佳实践
  1. HTML 文件:浏览器 no-cache + CDN s-maxage=60,发版后主动 Purge
  2. 带 hash 的资源:浏览器和 CDN 都 max-age=31536000,无需 Purge
  3. API 接口private, no-store,禁止 CDN 缓存
  4. 用户隐私页面private,确保 CDN 不缓存

Q8: 如何解决缓存导致的发版后用户看到旧页面的问题?

答案

发版后用户看到旧页面是前端部署中最常见的问题之一。根本原因是 HTML 入口文件被缓存,导致引用的仍然是旧版本的资源。解决方案需要从构建、部署、缓存配置三个层面综合处理。

问题分析

完整解决方案

方案一:HTML 使用协商缓存 + 资源文件使用内容哈希(核心方案)

nginx.conf - 确保 HTML 不被强缓存
# HTML 文件:每次都向服务器验证
location / {
try_files $uri /index.html;
add_header Cache-Control "no-cache";
# 双重保险:旧版浏览器兼容
add_header Pragma "no-cache";
}

# 带 hash 的资源:强缓存
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

方案二:部署时自动刷新 CDN 缓存

scripts/deploy.ts
import { execSync } from 'child_process';

interface DeployConfig {
cdnDomain: string;
purgeUrls: string[];
}

async function deploy(config: DeployConfig): Promise<void> {
// 1. 构建
console.log('构建项目...');
execSync('pnpm build', { stdio: 'inherit' });

// 2. 上传静态资源到 CDN/OSS
console.log('上传资源...');
execSync('aws s3 sync dist/ s3://my-bucket/ --cache-control "max-age=31536000"', {
stdio: 'inherit',
});

// 3. 上传 HTML(覆盖旧文件)
execSync(
'aws s3 cp dist/index.html s3://my-bucket/index.html --cache-control "no-cache"',
{ stdio: 'inherit' }
);

// 4. 刷新 CDN 缓存(关键步骤!)
console.log('刷新 CDN 缓存...');
await purgeCDN(config.purgeUrls);

console.log('部署完成!');
}

async function purgeCDN(urls: string[]): Promise<void> {
const response = await fetch('https://cdn-api.example.com/purge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CDN_API_KEY}`,
},
body: JSON.stringify({ urls }),
});

if (!response.ok) {
throw new Error(`CDN Purge 失败: ${response.statusText}`);
}
}

deploy({
cdnDomain: 'https://cdn.example.com',
purgeUrls: [
'https://cdn.example.com/',
'https://cdn.example.com/index.html',
],
});

方案三:Service Worker 版本管理

sw.ts - 确保 SW 不会缓存过期资源
const CACHE_VERSION = 'v20260228';  // 每次发版更新版本号
const CACHE_NAME = `app-${CACHE_VERSION}`;

// 安装新 SW 后立即激活
self.addEventListener('install', (event: ExtendableEvent) => {
self.skipWaiting(); // 跳过等待,立即激活新 SW
event.waitUntil(
caches.open(CACHE_NAME).then((cache) =>
cache.addAll(['/index.html', '/offline.html'])
)
);
});

// 激活时清理旧版本缓存
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => name !== CACHE_NAME) // 删除所有旧版本缓存
.map((name) => caches.delete(name))
)
).then(() => {
return self.clients.claim(); // 立即接管所有页面
})
);
});
main.ts - 页面端检测 SW 更新并提示用户
// 注册 SW 并检测更新
async function registerSW(): Promise<void> {
if (!('serviceWorker' in navigator)) return;

const registration = await navigator.serviceWorker.register('/sw.js');

// 检测到新 SW 可用时提示用户
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (!newWorker) return;

newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// 提示用户刷新页面获取最新版本
if (confirm('新版本已就绪,是否刷新页面?')) {
window.location.reload();
}
}
});
});
}

registerSW();

方案四:版本检测 + 强制刷新(兜底方案)

version-check.ts - 定期检查版本,发现更新后提示刷新
interface VersionInfo {
version: string;
buildTime: string;
}

class VersionChecker {
private currentVersion: string;
private checkInterval: number;
private timer: ReturnType<typeof setInterval> | null = null;

constructor(currentVersion: string, checkInterval = 5 * 60 * 1000) {
this.currentVersion = currentVersion;
this.checkInterval = checkInterval;
}

start(): void {
// 定期轮询版本文件
this.timer = setInterval(() => this.check(), this.checkInterval);

// 页面可见时也检查一次(用户切回标签页)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.check();
}
});
}

stop(): void {
if (this.timer) clearInterval(this.timer);
}

private async check(): Promise<void> {
try {
// 请求版本文件,跳过所有缓存
const res = await fetch('/version.json', { cache: 'no-store' });
const info: VersionInfo = await res.json();

if (info.version !== this.currentVersion) {
this.notifyUpdate(info.version);
}
} catch {
// 网络异常,静默失败
}
}

private notifyUpdate(newVersion: string): void {
console.log(`检测到新版本: ${newVersion},当前版本: ${this.currentVersion}`);

// 方式一:弹窗提示
if (confirm(`发现新版本 ${newVersion},是否刷新页面?`)) {
window.location.reload();
}

// 方式二:Toast 提示 + 手动刷新按钮(用户体验更好)
// showToast({ message: '发现新版本', action: () => location.reload() });
}
}

// 使用:在入口文件中启动
const checker = new VersionChecker(__APP_VERSION__);
checker.start();
public/version.json - 构建时自动生成
{
"version": "1.2.3",
"buildTime": "2026-02-28T10:00:00Z"
}
常见踩坑
  1. 只改 JS 不改 HTML:即使 JS 文件带了 hash,如果 HTML 被强缓存,用户仍然加载旧 HTML 中引用的旧 JS
  2. CDN 缓存未刷新:部署新版本后忘记 Purge CDN,导致 CDN 返回旧 HTML
  3. SW 缓存了 HTML:Service Worker 用 Cache First 缓存了 HTML,导致永远返回旧版本
  4. ?v= 参数方案的缺陷app.js?v=1.0.1 方式需要手动维护版本号,且部分 CDN 不识别 query string

各方案对比

方案可靠性实时性用户体验实现成本
HTML no-cache + hash无感
CDN Purge中(有延迟)无感
SW 版本管理需提示刷新
版本检测轮询需提示刷新
组合使用(推荐)最高较好
推荐方案组合
  1. 必选:HTML no-cache + 资源文件内容哈希
  2. 必选:部署后 Purge CDN
  3. 推荐:版本检测机制(兜底)
  4. 可选:Service Worker 版本管理(需要离线能力时)

相关链接