Skip to content

前端缓存策略完全指南

🎯 从 HTTP 缓存到 Service Worker,掌握 SPA 缓存最佳实践


1. 缓存体系总览

1.1 浏览器缓存层级

┌─────────────────────────────────────────────────────────────────────────┐
│                      浏览器缓存层级                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  优先级 (从高到低):                                                     │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  1. Memory Cache (内存缓存)                                      │   │
│  │     • 最快,页面关闭后清除                                        │   │
│  │     • 存储: JS、CSS、小图片                                       │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              ↓ 未命中                                   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  2. Service Worker Cache                                        │   │
│  │     • 可编程控制                                                  │   │
│  │     • 支持离线访问                                                │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              ↓ 未命中                                   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  3. Disk Cache (磁盘缓存)                                        │   │
│  │     • HTTP 缓存 (强缓存/协商缓存)                                 │   │
│  │     • 持久化存储                                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              ↓ 未命中                                   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  4. Push Cache (HTTP/2 推送缓存)                                 │   │
│  │     • 会话级别,连接关闭后清除                                    │   │
│  │     • 仅存在于 HTTP/2                                            │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              ↓ 未命中                                   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  5. Network (网络请求)                                           │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

1.2 HTTP 缓存分类

┌─────────────────────────────────────────────────────────────────────────┐
│                      HTTP 缓存分类                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌────────────────────────────┐    ┌────────────────────────────┐      │
│  │        强缓存               │    │        协商缓存             │      │
│  │   (不发送请求)              │    │   (发送请求验证)            │      │
│  ├────────────────────────────┤    ├────────────────────────────┤      │
│  │  Cache-Control: max-age    │    │  Last-Modified / If-M-S    │      │
│  │  Expires (已弃用)          │    │  ETag / If-None-Match      │      │
│  ├────────────────────────────┤    ├────────────────────────────┤      │
│  │  命中: 200 (from cache)    │    │  命中: 304 Not Modified    │      │
│  │  不命中: 发起请求           │    │  不命中: 200 + 新资源       │      │
│  └────────────────────────────┘    └────────────────────────────┘      │
│                                                                         │
│  执行顺序: 先检查强缓存 → 过期后走协商缓存                              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2. 强缓存详解

2.1 Cache-Control

http
# 常用指令
Cache-Control: max-age=31536000          # 缓存 1 年
Cache-Control: no-cache                  # 每次需协商验证
Cache-Control: no-store                  # 禁止缓存
Cache-Control: private                   # 仅浏览器缓存
Cache-Control: public                    # CDN 也可缓存
Cache-Control: immutable                 # 资源永不变化
Cache-Control: stale-while-revalidate=60 # 后台刷新

# 组合使用
Cache-Control: public, max-age=31536000, immutable   # 静态资源
Cache-Control: private, no-cache                     # 动态 HTML
Cache-Control: no-store                              # 敏感数据

2.2 指令详解

┌─────────────────────────────────────────────────────────────────────────┐
│                   Cache-Control 指令详解                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  指令                      │ 说明                                       │
│  ─────────────────────────┼───────────────────────────────────────────  │
│  max-age=<seconds>        │ 缓存有效期 (相对时间)                       │
│  s-maxage=<seconds>       │ CDN/代理缓存有效期 (覆盖 max-age)           │
│  no-cache                 │ 可缓存,但每次必须向服务器验证               │
│  no-store                 │ 完全不缓存                                  │
│  private                  │ 仅浏览器缓存 (不允许 CDN)                   │
│  public                   │ 允许 CDN 缓存                               │
│  immutable                │ 资源不会变化 (避免刷新时重新验证)            │
│  must-revalidate          │ 过期后必须验证,不能使用过期缓存             │
│  stale-while-revalidate   │ 返回过期缓存,同时后台刷新                   │
│  stale-if-error           │ 出错时可使用过期缓存                         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2.3 缓存决策流程图

┌─────────────────────────────────────────────────────────────────────────┐
│                       浏览器缓存决策流程                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│                         请求资源                                        │
│                            │                                            │
│                            ▼                                            │
│                    ┌───────────────┐                                    │
│                    │ 有本地缓存?   │                                    │
│                    └───────┬───────┘                                    │
│                       Yes │ │ No                                        │
│              ┌────────────┘ └────────────┐                              │
│              ▼                           ▼                              │
│      ┌───────────────┐           ┌───────────────┐                      │
│      │ Cache-Control │           │  发起请求     │                      │
│      │ max-age 过期? │           │  200 OK       │                      │
│      └───────┬───────┘           └───────────────┘                      │
│         No  │ │ Yes                                                     │
│    ┌────────┘ └────────┐                                                │
│    ▼                   ▼                                                │
│  强缓存命中          协商缓存                                           │
│  ┌───────────────┐  ┌───────────────┐                                   │
│  │ 200 (from     │  │ If-None-Match │                                   │
│  │  memory/disk  │  │ If-Modified-  │                                   │
│  │  cache)       │  │ Since         │                                   │
│  └───────────────┘  └───────┬───────┘                                   │
│                             │                                           │
│                             ▼                                           │
│                     ┌───────────────┐                                   │
│                     │ 资源变化?     │                                   │
│                     └───────┬───────┘                                   │
│                        Yes │ │ No                                       │
│                   ┌────────┘ └────────┐                                 │
│                   ▼                   ▼                                 │
│                 200 OK             304 Not Modified                     │
│                 新资源             使用本地缓存                          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3. 协商缓存详解

3.1 Last-Modified / If-Modified-Since

http
# 首次请求响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
Cache-Control: no-cache

# 后续请求
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

# 未修改
HTTP/1.1 304 Not Modified

# 已修改
HTTP/1.1 200 OK
Last-Modified: Thu, 22 Oct 2023 10:30:00 GMT

3.2 ETag / If-None-Match

http
# 首次请求响应
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: no-cache

# 后续请求
GET /style.css HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 未修改
HTTP/1.1 304 Not Modified

# 已修改
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6g7h8i9j0"

3.3 ETag vs Last-Modified

┌─────────────────────────────────────────────────────────────────────────┐
│                    ETag vs Last-Modified                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  维度          │ Last-Modified          │ ETag                          │
│  ─────────────┼───────────────────────┼───────────────────────────────  │
│  精度          │ 秒级                   │ 字节级                         │
│  生成方式      │ 文件修改时间           │ 内容哈希/版本号                │
│  性能          │ 高 (直接读取)          │ 较低 (需计算)                  │
│  可靠性        │ 可能不准确*            │ 精确                          │
│  优先级        │ 低                     │ 高 (同时存在时优先)            │
│                                                                         │
│  * Last-Modified 不准确的情况:                                          │
│    1. 文件编辑后撤销,时间变了但内容没变                                 │
│    2. 1 秒内多次修改,时间相同但内容不同                                 │
│    3. 负载均衡下,不同服务器文件时间可能不一致                           │
│                                                                         │
│  最佳实践: 同时使用 ETag + Last-Modified                                │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4. SPA 缓存最佳实践

4.1 资源缓存策略

┌─────────────────────────────────────────────────────────────────────────┐
│                    SPA 资源缓存策略                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  资源类型              │ 缓存策略                                       │
│  ─────────────────────┼───────────────────────────────────────────────  │
│  index.html           │ no-cache (每次验证)                            │
│  JS/CSS (带 hash)     │ max-age=31536000, immutable (永久缓存)         │
│  图片 (带 hash)       │ max-age=31536000, immutable                    │
│  字体                 │ max-age=31536000                               │
│  API 响应             │ no-store 或 短期缓存                           │
│                                                                         │
│  关键原则:                                                              │
│  1. HTML 不能强缓存 (否则无法更新 JS/CSS 引用)                          │
│  2. 静态资源加 hash,配合长期缓存                                       │
│  3. API 根据业务需求决定是否缓存                                        │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4.2 Nginx 配置示例

nginx
server {
    listen 80;
    server_name app.example.com;
    root /var/www/app/dist;

    # HTML - 不缓存,每次验证
    location / {
        try_files $uri $uri/ /index.html;
        
        add_header Cache-Control "no-cache";
        # 或者使用协商缓存
        # add_header Cache-Control "no-cache, must-revalidate";
        
        # 安全头
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-Content-Type-Options "nosniff";
    }

    # 带 hash 的静态资源 - 永久缓存
    location ~* \.(js|css)$ {
        # 检查文件名是否包含 hash (如 main.a1b2c3d4.js)
        if ($uri ~* "\.([a-f0-9]{8,})\.") {
            add_header Cache-Control "public, max-age=31536000, immutable";
        }
        
        # 不带 hash 的 JS/CSS (兜底)
        add_header Cache-Control "public, max-age=86400";
    }

    # 图片/字体 - 长期缓存
    location ~* \.(ico|gif|jpg|jpeg|png|webp|svg|woff|woff2|ttf|eot)$ {
        add_header Cache-Control "public, max-age=31536000";
        
        # 启用 gzip
        gzip on;
        gzip_types image/svg+xml application/font-woff application/font-woff2;
    }

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

4.3 Vite/Webpack 文件名 Hash

javascript
// vite.config.ts
export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                // 入口文件
                entryFileNames: 'assets/[name].[hash].js',
                // 代码分割
                chunkFileNames: 'assets/[name].[hash].js',
                // 资源文件
                assetFileNames: 'assets/[name].[hash].[ext]',
            },
        },
    },
});

// webpack.config.js
module.exports = {
    output: {
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].js',
    },
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif|svg)$/,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[name].[hash][ext]',
                },
            },
        ],
    },
};

5. Service Worker 缓存

5.1 缓存策略

┌─────────────────────────────────────────────────────────────────────────┐
│                   Service Worker 缓存策略                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. Cache First (缓存优先)                                              │
│  ─────────────────────────                                              │
│  请求 → 缓存命中? → 是 → 返回缓存                                       │
│                   → 否 → 网络请求 → 缓存 → 返回                         │
│  适用: 静态资源 (JS/CSS/图片)                                           │
│                                                                         │
│  2. Network First (网络优先)                                            │
│  ─────────────────────────                                              │
│  请求 → 网络请求 → 成功 → 缓存 → 返回                                   │
│                   → 失败 → 返回缓存                                     │
│  适用: API 请求、需要实时数据的场景                                     │
│                                                                         │
│  3. Stale While Revalidate (先返回缓存,后台更新)                       │
│  ────────────────────────────────────────────                           │
│  请求 → 返回缓存 (立即) → 同时发起网络请求 → 更新缓存                   │
│  适用: 用户头像、非关键内容                                             │
│                                                                         │
│  4. Cache Only (仅缓存)                                                 │
│  ─────────────────────                                                  │
│  请求 → 返回缓存 (无网络请求)                                           │
│  适用: 离线应用的静态资源                                               │
│                                                                         │
│  5. Network Only (仅网络)                                               │
│  ─────────────────────                                                  │
│  请求 → 网络请求 (不使用缓存)                                           │
│  适用: 敏感数据、不能缓存的内容                                         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

5.2 Workbox 实现

javascript
// sw.js (使用 Workbox)
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { 
    CacheFirst, 
    NetworkFirst, 
    StaleWhileRevalidate 
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

// 预缓存 (构建时注入)
precacheAndRoute(self.__WB_MANIFEST);

// 静态资源 - Cache First
registerRoute(
    ({ request }) => 
        request.destination === 'script' ||
        request.destination === 'style' ||
        request.destination === 'image',
    new CacheFirst({
        cacheName: 'static-resources',
        plugins: [
            new ExpirationPlugin({
                maxEntries: 100,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 天
            }),
        ],
    })
);

// API 请求 - Network First
registerRoute(
    ({ url }) => url.pathname.startsWith('/api/'),
    new NetworkFirst({
        cacheName: 'api-cache',
        networkTimeoutSeconds: 10,
        plugins: [
            new ExpirationPlugin({
                maxEntries: 50,
                maxAgeSeconds: 5 * 60, // 5 分钟
            }),
        ],
    })
);

// 字体 - Stale While Revalidate
registerRoute(
    ({ request }) => request.destination === 'font',
    new StaleWhileRevalidate({
        cacheName: 'fonts',
    })
);

// HTML - Network First (确保获取最新版本)
registerRoute(
    ({ request }) => request.mode === 'navigate',
    new NetworkFirst({
        cacheName: 'pages',
        networkTimeoutSeconds: 3,
    })
);

5.3 手动实现缓存策略

javascript
// sw.js (手动实现)

const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
    '/',
    '/index.html',
    '/main.js',
    '/style.css',
];

// 安装 - 预缓存
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME).then((cache) => {
            return cache.addAll(STATIC_ASSETS);
        })
    );
    self.skipWaiting();
});

// 激活 - 清理旧缓存
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames
                    .filter((name) => name !== CACHE_NAME)
                    .map((name) => caches.delete(name))
            );
        })
    );
    self.clients.claim();
});

// 请求拦截
self.addEventListener('fetch', (event) => {
    const { request } = event;
    const url = new URL(request.url);
    
    // API 请求 - Network First
    if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirst(request));
        return;
    }
    
    // 静态资源 - Cache First
    event.respondWith(cacheFirst(request));
});

// Cache First 策略
async function cacheFirst(request) {
    const cached = await caches.match(request);
    if (cached) return cached;
    
    try {
        const response = await fetch(request);
        const cache = await caches.open(CACHE_NAME);
        cache.put(request, response.clone());
        return response;
    } catch {
        return new Response('Offline', { status: 503 });
    }
}

// Network First 策略
async function networkFirst(request) {
    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 || new Response('Offline', { status: 503 });
    }
}

6. CDN 缓存

6.1 CDN 缓存配置

┌─────────────────────────────────────────────────────────────────────────┐
│                        CDN 缓存架构                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│                        ┌──────────┐                                     │
│                        │  Origin  │                                     │
│                        │  Server  │                                     │
│                        └────┬─────┘                                     │
│                             │                                           │
│              ┌──────────────┼──────────────┐                            │
│              │              │              │                            │
│        ┌─────▼────┐  ┌─────▼────┐  ┌─────▼────┐                        │
│        │ CDN Edge │  │ CDN Edge │  │ CDN Edge │                        │
│        │  (北京)   │  │  (上海)   │  │  (深圳)   │                        │
│        └─────┬────┘  └─────┬────┘  └─────┬────┘                        │
│              │              │              │                            │
│        ┌─────▼────┐  ┌─────▼────┐  ┌─────▼────┐                        │
│        │  User A  │  │  User B  │  │  User C  │                        │
│        └──────────┘  └──────────┘  └──────────┘                        │
│                                                                         │
│  缓存生效顺序:                                                          │
│  1. 用户请求 → 最近的 CDN Edge 节点                                     │
│  2. CDN 节点检查本地缓存                                                │
│  3. 缓存命中 → 直接返回                                                 │
│  4. 缓存未命中 → 回源到 Origin → 缓存 → 返回                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

6.2 CDN 缓存 Header

http
# 源站响应 (控制 CDN 缓存)
Cache-Control: public, s-maxage=86400, max-age=3600
# s-maxage: CDN 缓存 1 天
# max-age: 浏览器缓存 1 小时

# Vary: 根据不同 Header 缓存不同版本
Vary: Accept-Encoding
# CDN 会分别缓存 gzip 和 br 压缩版本

# CDN 厂商特定 Header
# Cloudflare
Cloudflare-CDN-Cache-Control: max-age=86400
# Fastly
Surrogate-Control: max-age=86400

6.3 CDN 缓存刷新

javascript
// 方案 1: 版本化 URL (推荐)
// 文件名带 hash,无需刷新 CDN
<script src="/main.a1b2c3d4.js"></script>

// 方案 2: Query String
// 可能被某些 CDN 忽略
<script src="/main.js?v=20231023"></script>

// 方案 3: API 刷新
// Cloudflare API
await fetch('https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache', {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer {api_token}',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        files: [
            'https://example.com/main.js',
            'https://example.com/style.css',
        ],
    }),
});

7. 缓存问题排查

7.1 DevTools 查看缓存

Chrome DevTools → Network:
- Size 列: 显示 "(memory cache)" 或 "(disk cache)"
- 右键 → Clear browser cache: 清除缓存
- Disable cache: 禁用缓存 (调试用)

Application → Cache Storage:
- 查看 Service Worker 缓存内容

Application → Storage:
- Clear site data: 清除所有缓存

7.2 常见问题

┌─────────────────────────────────────────────────────────────────────────┐
│                      缓存常见问题排查                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  问题: 更新后用户看到旧版本                                             │
│  ─────────────────────────────                                          │
│  原因:                                                                  │
│  1. HTML 被强缓存 (应该用 no-cache)                                     │
│  2. CDN 缓存未刷新                                                      │
│  3. Service Worker 返回旧缓存                                           │
│                                                                         │
│  解决:                                                                  │
│  1. HTML 使用 Cache-Control: no-cache                                   │
│  2. 静态资源文件名加 hash                                               │
│  3. 更新 Service Worker 版本号                                          │
│                                                                         │
│  问题: 协商缓存不生效                                                   │
│  ───────────────────────                                                │
│  原因:                                                                  │
│  1. 服务器未返回 ETag/Last-Modified                                     │
│  2. Cache-Control: no-store 禁止了缓存                                  │
│  3. 请求带了 Cache-Control: no-cache (强制请求)                         │
│                                                                         │
│  问题: 缓存导致的白屏                                                   │
│  ───────────────────────                                                │
│  原因:                                                                  │
│  1. HTML 缓存了旧的 JS 引用路径                                         │
│  2. 新 JS 文件已上传,但 HTML 还引用旧的 hash                            │
│                                                                         │
│  解决:                                                                  │
│  1. 先上传新的 JS/CSS,再上传 HTML                                      │
│  2. 保留旧版本文件一段时间                                              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

8. 面试高频问题

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

类型请求Header状态码
强缓存不发送Cache-Control, Expires200 (from cache)
协商缓存发送验证ETag, Last-Modified304 Not Modified

Q2: no-cache 和 no-store 的区别?

  • no-cache: 可以缓存,但每次使用前必须向服务器验证
  • no-store: 完全不缓存,每次都从服务器获取

Q3: ETag 和 Last-Modified 哪个优先级高?

ETag 优先级高。当同时存在时,服务器会先检查 ETag,再检查 Last-Modified。

Q4: SPA 为什么 HTML 不能强缓存?

HTML 包含对 JS/CSS 的引用。如果 HTML 被强缓存,更新后用户可能加载旧 HTML,引用不存在的旧 JS 文件,导致白屏。

Q5: 如何实现"更新后用户立即看到新版本"?

  1. HTML 使用 Cache-Control: no-cache
  2. JS/CSS 文件名带 contenthash
  3. 每次构建生成新 hash
  4. 用户刷新时获取新 HTML,自动加载新 JS/CSS

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

Service Worker 缓存优先级高于 HTTP 缓存。请求会先经过 SW,SW 可以决定使用缓存还是发起网络请求。网络请求仍然遵循 HTTP 缓存规则。

前端面试知识库