Skip to content

缓存策略

课程概述

本章节深入探讨Web应用的缓存策略,学习如何通过合理的缓存配置提升应用性能和用户体验。掌握缓存策略是Web性能优化的核心技能。

学习目标

  • 理解浏览器缓存机制
  • 掌握HTTP缓存头配置
  • 学习Service Worker缓存
  • 了解CDN缓存策略
  • 掌握缓存更新和失效
  • 学习缓存最佳实践

第一部分:缓存基础

1.1 什么是缓存

缓存是将资源副本存储在本地,避免重复请求,加快访问速度的技术。

缓存层级:

javascript
1. 浏览器缓存    - Memory/Disk Cache
2. CDN缓存      - 边缘节点缓存
3. 代理缓存      - Proxy Cache
4. 服务器缓存    - Server Cache
5. 数据库缓存    - Database Cache

性能对比:

javascript
无缓存:
请求 → 网络传输 → 服务器处理 → 响应
时间: 500ms

有缓存:
请求 → 本地缓存 → 响应
时间: 5ms

1.2 缓存的好处

javascript
1. 减少网络请求    - 节省带宽
2. 加快页面加载    - 提升性能
3. 降低服务器负载  - 减少压力
4. 改善用户体验    - 即时响应
5. 离线访问       - PWA支持

1.3 缓存类型

javascript
// 强缓存
Cache-Control: max-age=31536000
不发送请求,直接使用缓存

// 协商缓存
If-None-Match: "abc123"
发送请求,服务器判断是否更新

// 无缓存
Cache-Control: no-store
每次都重新请求

第二部分:HTTP缓存

2.1 Cache-Control

javascript
// 常用指令
Cache-Control: public              // 可被任何缓存
Cache-Control: private             // 仅浏览器缓存
Cache-Control: no-cache            // 需要验证
Cache-Control: no-store            // 不缓存
Cache-Control: max-age=3600        // 缓存1小时
Cache-Control: must-revalidate     // 过期必须验证
Cache-Control: immutable           // 不可变,不验证

组合使用:

javascript
// HTML - 不缓存或短期缓存
Cache-Control: no-cache
// 或
Cache-Control: public, max-age=0, must-revalidate

// JS/CSS(带hash) - 永久缓存
Cache-Control: public, max-age=31536000, immutable

// 图片 - 长期缓存
Cache-Control: public, max-age=31536000

// API - 不缓存
Cache-Control: no-store, private

2.2 Expires

javascript
// 过期时间(HTTP/1.0)
Expires: Wed, 21 Oct 2025 07:28:00 GMT

// 问题:依赖客户端时间
// 被Cache-Control取代

2.3 ETag

javascript
// 服务器响应
ETag: "abc123"

// 客户端请求
If-None-Match: "abc123"

// 服务器响应
304 Not Modified (缓存有效)
// 或
200 OK (返回新内容)

ETag生成:

javascript
// Node.js示例
const etag = require('etag')

app.get('/api/data', (req, res) => {
  const data = getData()
  const tag = etag(JSON.stringify(data))
  
  res.set('ETag', tag)
  
  if (req.headers['if-none-match'] === tag) {
    return res.status(304).send()
  }
  
  res.json(data)
})

2.4 Last-Modified

javascript
// 服务器响应
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

// 客户端请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

// 服务器响应
304 Not Modified
// 或
200 OK

2.5 Vary头

javascript
// 根据请求头变化缓存
Vary: Accept-Encoding

// 多个条件
Vary: Accept-Encoding, Accept-Language

// 示例
GET /app.js
Accept-Encoding: gzip
→ 缓存 app.js (gzip版本)

GET /app.js
Accept-Encoding: br
→ 缓存 app.js (brotli版本)

第三部分:Vite缓存配置

3.1 文件命名策略

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

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 使用contenthash
        entryFileNames: 'js/[name]-[hash].js',
        chunkFileNames: 'js/[name]-[hash].js',
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split('.')
          const ext = info[info.length - 1]
          
          if (/png|jpe?g|svg|gif/i.test(ext)) {
            return `images/[name]-[hash][extname]`
          }
          
          if (/woff2?|ttf|eot/i.test(ext)) {
            return `fonts/[name]-[hash][extname]`
          }
          
          return `assets/[name]-[hash][extname]`
        }
      }
    }
  }
})

输出结果:

dist/
├── js/
│   ├── index-abc123.js      # 带hash,可永久缓存
│   └── vendor-def456.js
├── css/
│   └── main-ghi789.css
├── images/
│   └── logo-jkl012.png
└── index.html               # 不带hash,不缓存

3.2 HTML处理

html
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <!-- 资源带hash,可永久缓存 -->
    <link rel="stylesheet" href="/css/main-ghi789.css">
    <script type="module" src="/js/index-abc123.js"></script>
  </head>
</html>

HTML缓存策略:

javascript
// 方式1: 不缓存
Cache-Control: no-cache

// 方式2: 短期缓存
Cache-Control: public, max-age=300, must-revalidate

// 方式3: 协商缓存
Cache-Control: no-cache
ETag: "xyz789"

第四部分:服务端缓存配置

4.1 Nginx配置

nginx
# nginx.conf
server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    
    # HTML - 不缓存
    location ~* \.html$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        expires -1;
    }
    
    # JS/CSS(带hash) - 永久缓存
    location ~* \.(js|css)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        expires 1y;
        access_log off;
    }
    
    # 图片 - 长期缓存
    location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
        add_header Cache-Control "public, max-age=31536000";
        expires 1y;
        access_log off;
    }
    
    # 字体 - 永久缓存
    location ~* \.(woff2?|ttf|otf|eot)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Access-Control-Allow-Origin "*";
        expires 1y;
        access_log off;
    }
    
    # API - 不缓存
    location /api/ {
        add_header Cache-Control "no-store, private";
        proxy_pass http://backend;
    }
}

4.2 Node.js/Express配置

javascript
// server.js
const express = require('express')
const path = require('path')

const app = express()

// 静态文件缓存
app.use('/static', express.static('dist', {
  maxAge: '1y',
  immutable: true,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache')
    }
  }
}))

// HTML不缓存
app.get('*.html', (req, res) => {
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
  res.sendFile(path.join(__dirname, 'dist', req.path))
})

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

app.listen(3000)

4.3 Apache配置

apache
# .htaccess
<IfModule mod_expires.c>
    ExpiresActive On
    
    # HTML - 不缓存
    ExpiresByType text/html "access plus 0 seconds"
    
    # JS/CSS - 1年
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
    
    # 图片 - 1年
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"
    
    # 字体 - 1年
    ExpiresByType font/woff2 "access plus 1 year"
</IfModule>

<IfModule mod_headers.c>
    # JS/CSS - immutable
    <FilesMatch "\.(js|css)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
    
    # HTML - no-cache
    <FilesMatch "\.html$">
        Header set Cache-Control "no-cache, no-store, must-revalidate"
    </FilesMatch>
</IfModule>

第五部分:Service Worker缓存

5.1 基础Service Worker

javascript
// sw.js
const CACHE_NAME = 'my-app-v1'
const CACHE_URLS = [
  '/',
  '/index.html',
  '/css/main.css',
  '/js/app.js',
  '/images/logo.png'
]

// 安装
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(CACHE_URLS)
    })
  )
})

// 激活
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName)
          }
        })
      )
    })
  )
})

// 请求拦截
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request)
    })
  )
})

5.2 缓存策略

1. Cache First (缓存优先):

javascript
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 优先使用缓存
      if (response) {
        return response
      }
      // 缓存未命中,请求网络
      return fetch(event.request).then((response) => {
        // 缓存新请求的响应
        return caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, response.clone())
          return response
        })
      })
    })
  )
})

2. Network First (网络优先):

javascript
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // 更新缓存
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, response.clone())
        })
        return response
      })
      .catch(() => {
        // 网络失败,使用缓存
        return caches.match(event.request)
      })
  )
})

3. Stale While Revalidate:

javascript
self.addEventListener('fetch', (event) => {
  event.respondWith(
    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
      })
    })
  )
})

5.3 Workbox

bash
npm install -D workbox-webpack-plugin
javascript
// webpack.config.js
const { GenerateSW } = require('workbox-webpack-plugin')

module.exports = {
  plugins: [
    new GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      
      // 预缓存
      include: [/\.html$/, /\.js$/, /\.css$/],
      
      // 运行时缓存
      runtimeCaching: [
        {
          urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
          handler: 'CacheFirst',
          options: {
            cacheName: 'images',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
            }
          }
        },
        {
          urlPattern: /^https:\/\/api\.example\.com/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api',
            expiration: {
              maxEntries: 100,
              maxAgeSeconds: 60 * 60, // 1小时
            }
          }
        }
      ]
    })
  ]
}

第六部分:缓存更新策略

6.1 版本化缓存

javascript
// sw.js
const VERSION = '1.0.0'
const CACHE_NAME = `my-app-${VERSION}`

// 更新版本时自动清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME)
          .map(name => caches.delete(name))
      )
    })
  )
})

6.2 内容Hash

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 内容变化时hash变化
        entryFileNames: 'js/[name]-[hash].js',
        
        // 使用contenthash而不是hash
        assetFileNames: 'assets/[name]-[hash][extname]'
      }
    }
  }
})

工作原理:

文件内容变化 → hash变化 → URL变化 → 缓存失效

app-abc123.js (旧版本,已缓存)
↓ 代码更新
app-def456.js (新版本,新URL)

6.3 缓存破坏

javascript
// 查询参数方式(不推荐)
<script src="/app.js?v=1.0.0"></script>

// Hash方式(推荐)
<script src="/app-abc123.js"></script>

6.4 强制刷新

javascript
// 客户端强制刷新
function forceRefresh() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.getRegistrations().then((registrations) => {
      registrations.forEach((registration) => {
        registration.unregister()
      })
    })
  }
  
  // 清除所有缓存
  caches.keys().then((cacheNames) => {
    cacheNames.forEach((cacheName) => {
      caches.delete(cacheName)
    })
  })
  
  // 重新加载
  window.location.reload(true)
}

第七部分:最佳实践

7.1 缓存策略总结

javascript
资源类型              缓存策略                     Cache-Control
--------------------------------------------------------------------
HTML                 不缓存/短期缓存              no-cache
JS/CSS(带hash)       永久缓存                     public, max-age=31536000, immutable
JS/CSS(不带hash)     短期缓存                     public, max-age=3600
图片(带hash)         永久缓存                     public, max-age=31536000
图片(不带hash)       长期缓存                     public, max-age=604800
字体                 永久缓存                     public, max-age=31536000, immutable
API响应              不缓存                       no-store, private
用户数据             不缓存                       no-store, private
公共数据             短期缓存                     public, max-age=300

7.2 完整配置示例

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

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'js/[name]-[hash].js',
        chunkFileNames: 'js/[name]-[hash].js',
        assetFileNames: (assetInfo) => {
          const ext = assetInfo.name.split('.').pop()
          
          if (/png|jpe?g|svg|gif|webp/i.test(ext!)) {
            return 'images/[name]-[hash][extname]'
          }
          
          if (/woff2?|ttf|otf|eot/i.test(ext!)) {
            return 'fonts/[name]-[hash][extname]'
          }
          
          return 'assets/[name]-[hash][extname]'
        }
      }
    }
  }
})
nginx
# nginx.conf
server {
    # HTML
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
        etag on;
    }
    
    # 带hash的资源
    location ~* -[a-f0-9]{8}\.(js|css|png|jpg|svg|woff2)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }
    
    # 其他静态资源
    location ~* \.(js|css|png|jpg)$ {
        add_header Cache-Control "public, max-age=3600";
    }
}

7.3 缓存检查清单

javascript
构建阶段:
☑ 使用contenthash命名
☑ 分离vendor和业务代码
☑ 压缩资源文件
☑ 生成source map(可选)

部署阶段:
☑ HTML不缓存或短期缓存
JS/CSS永久缓存(带hash)
☑ 图片长期缓存
☑ 字体永久缓存
☑ API不缓存

验证阶段:
☑ 检查响应头
☑ 验证缓存命中
☑ 测试缓存更新
☑ 监控缓存效果

第八部分:缓存性能监控

8.1 缓存命中率监控

typescript
class CacheMonitor {
  private metrics = {
    hits: 0,
    misses: 0,
    bypassed: 0
  };
  
  init() {
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          const resource = entry as PerformanceResourceTiming;
          
          // 判断缓存状态
          if (resource.transferSize === 0 && resource.decodedBodySize > 0) {
            this.metrics.hits++; // 缓存命中
          } else if (resource.transferSize > 0) {
            this.metrics.misses++; // 缓存未命中
          }
        }
      });
      
      observer.observe({ entryTypes: ['resource'] });
    }
  }
  
  getReport() {
    const total = this.metrics.hits + this.metrics.misses;
    const hitRate = total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : '0';
    
    return {
      hits: this.metrics.hits,
      misses: this.metrics.misses,
      total,
      hitRate: hitRate + '%'
    };
  }
}

const cacheMonitor = new CacheMonitor();
cacheMonitor.init();

8.2 Service Worker缓存分析

typescript
// 分析Service Worker缓存使用情况
async function analyzeCacheStorage() {
  if ('caches' in window) {
    const cacheNames = await caches.keys();
    const cacheStats = [];
    
    for (const cacheName of cacheNames) {
      const cache = await caches.open(cacheName);
      const keys = await cache.keys();
      
      let totalSize = 0;
      for (const request of keys) {
        const response = await cache.match(request);
        if (response) {
          const blob = await response.blob();
          totalSize += blob.size;
        }
      }
      
      cacheStats.push({
        name: cacheName,
        entries: keys.length,
        size: (totalSize / 1024 / 1024).toFixed(2) + 'MB'
      });
    }
    
    return cacheStats;
  }
}

// 使用
analyzeCacheStorage().then(stats => {
  console.table(stats);
});

8.3 缓存效果可视化

typescript
// 可视化缓存命中率
class CacheVisualizer {
  private chart: any;
  
  init() {
    const canvas = document.createElement('canvas');
    canvas.id = 'cache-chart';
    document.body.appendChild(canvas);
    
    // 使用Chart.js绘制
    this.chart = new Chart(canvas, {
      type: 'doughnut',
      data: {
        labels: ['缓存命中', '缓存未命中'],
        datasets: [{
          data: [0, 0],
          backgroundColor: ['#4CAF50', '#F44336']
        }]
      },
      options: {
        title: {
          display: true,
          text: '缓存命中率'
        }
      }
    });
  }
  
  update(hits: number, misses: number) {
    this.chart.data.datasets[0].data = [hits, misses];
    this.chart.update();
  }
}

第九部分:高级缓存策略

9.1 智能预缓存

typescript
// 基于用户行为的智能预缓存
class SmartPreCache {
  private accessLog: Map<string, number> = new Map();
  
  recordAccess(url: string) {
    const count = this.accessLog.get(url) || 0;
    this.accessLog.set(url, count + 1);
    
    // 分析高频访问资源
    this.analyzeAndPreCache();
  }
  
  async analyzeAndPreCache() {
    // 找出访问频率最高的资源
    const sorted = Array.from(this.accessLog.entries())
      .sort(([, a], [, b]) => b - a)
      .slice(0, 10);
    
    // 预缓存高频资源
    const cache = await caches.open('smart-precache-v1');
    
    for (const [url, count] of sorted) {
      if (count > 3) {
        const cached = await cache.match(url);
        if (!cached) {
          try {
            const response = await fetch(url);
            await cache.put(url, response);
            console.log(`预缓存: ${url}`);
          } catch (error) {
            console.error(`预缓存失败: ${url}`, error);
          }
        }
      }
    }
  }
}

const smartCache = new SmartPreCache();

// 拦截所有fetch请求
self.addEventListener('fetch', (event) => {
  smartCache.recordAccess(event.request.url);
});

9.2 分层缓存策略

typescript
// 多层缓存架构
class LayeredCache {
  // L1: Memory Cache (最快)
  private memoryCache: Map<string, any> = new Map();
  
  // L2: IndexedDB (持久化)
  private db: IDBDatabase;
  
  // L3: Service Worker Cache API
  private swCache: Cache;
  
  async init() {
    this.db = await this.openDB();
    this.swCache = await caches.open('layered-cache-v1');
  }
  
  async get(key: string): Promise<any> {
    // L1: 内存缓存
    if (this.memoryCache.has(key)) {
      console.log('L1 Cache Hit');
      return this.memoryCache.get(key);
    }
    
    // L2: IndexedDB
    const indexedData = await this.getFromIndexedDB(key);
    if (indexedData) {
      console.log('L2 Cache Hit');
      this.memoryCache.set(key, indexedData); // 回填L1
      return indexedData;
    }
    
    // L3: Service Worker Cache
    const response = await this.swCache.match(key);
    if (response) {
      console.log('L3 Cache Hit');
      const data = await response.json();
      this.memoryCache.set(key, data); // 回填L1
      await this.putToIndexedDB(key, data); // 回填L2
      return data;
    }
    
    console.log('Cache Miss');
    return null;
  }
  
  async set(key: string, value: any, ttl?: number) {
    // 写入所有层级
    this.memoryCache.set(key, value);
    await this.putToIndexedDB(key, value, ttl);
    await this.swCache.put(key, new Response(JSON.stringify(value)));
  }
  
  private openDB(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('LayeredCache', 1);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains('cache')) {
          db.createObjectStore('cache', { keyPath: 'key' });
        }
      };
    });
  }
  
  private async getFromIndexedDB(key: string): Promise<any> {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['cache'], 'readonly');
      const store = transaction.objectStore('cache');
      const request = store.get(key);
      
      request.onsuccess = () => {
        const result = request.result;
        if (result && (!result.expiry || result.expiry > Date.now())) {
          resolve(result.value);
        } else {
          resolve(null);
        }
      };
      request.onerror = () => reject(request.error);
    });
  }
  
  private async putToIndexedDB(key: string, value: any, ttl?: number): Promise<void> {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['cache'], 'readwrite');
      const store = transaction.objectStore('cache');
      const data = {
        key,
        value,
        expiry: ttl ? Date.now() + ttl : null
      };
      const request = store.put(data);
      
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}

9.3 条件缓存

typescript
// 基于条件的智能缓存
class ConditionalCache {
  async shouldCache(request: Request, response: Response): Promise<boolean> {
    // 1. 检查HTTP状态
    if (!response.ok) {
      return false;
    }
    
    // 2. 检查请求方法
    if (request.method !== 'GET') {
      return false;
    }
    
    // 3. 检查响应大小
    const contentLength = response.headers.get('Content-Length');
    if (contentLength && parseInt(contentLength) > 5 * 1024 * 1024) {
      // 不缓存大于5MB的文件
      return false;
    }
    
    // 4. 检查Content-Type
    const contentType = response.headers.get('Content-Type');
    const cachableTypes = [
      'text/html',
      'text/css',
      'application/javascript',
      'application/json',
      'image/'
    ];
    
    if (!cachableTypes.some(type => contentType?.includes(type))) {
      return false;
    }
    
    // 5. 检查用户偏好
    const connection = (navigator as any).connection;
    if (connection && connection.saveData) {
      // 省流量模式,只缓存关键资源
      return request.url.includes('/critical/');
    }
    
    return true;
  }
  
  async cache(request: Request, response: Response) {
    if (await this.shouldCache(request, response)) {
      const cache = await caches.open('conditional-cache-v1');
      await cache.put(request, response.clone());
    }
  }
}

第十部分:缓存最佳实践

10.1 完整缓存方案

typescript
// 生产环境完整缓存配置
const cacheStrategy = {
  // 1. 静态资源 - 长期缓存
  static: {
    pattern: /\.(js|css|png|jpg|jpeg|gif|svg|woff2)$/,
    strategy: 'CacheFirst',
    cacheName: 'static-cache-v1',
    maxAge: 365 * 24 * 60 * 60, // 1年
    maxEntries: 100
  },
  
  // 2. API - 网络优先
  api: {
    pattern: /\/api\//,
    strategy: 'NetworkFirst',
    cacheName: 'api-cache-v1',
    maxAge: 5 * 60, // 5分钟
    maxEntries: 50
  },
  
  // 3. 页面 - 网络优先with快速回退
  pages: {
    pattern: /\.html$/,
    strategy: 'NetworkFirst',
    cacheName: 'pages-cache-v1',
    maxAge: 24 * 60 * 60, // 1天
    networkTimeout: 3000 // 3秒超时
  },
  
  // 4. 图片 - 缓存优先
  images: {
    pattern: /\.(png|jpg|jpeg|gif|webp)$/,
    strategy: 'CacheFirst',
    cacheName: 'images-cache-v1',
    maxAge: 30 * 24 * 60 * 60, // 30天
    maxEntries: 200
  }
};

// Workbox实现
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// 注册路由
Object.entries(cacheStrategy).forEach(([name, config]) => {
  const strategy = config.strategy === 'CacheFirst'
    ? new CacheFirst({
        cacheName: config.cacheName,
        plugins: [
          new ExpirationPlugin({
            maxAgeSeconds: config.maxAge,
            maxEntries: config.maxEntries
          }),
          new CacheableResponsePlugin({
            statuses: [0, 200]
          })
        ]
      })
    : new NetworkFirst({
        cacheName: config.cacheName,
        networkTimeoutSeconds: config.networkTimeout || 10,
        plugins: [
          new ExpirationPlugin({
            maxAgeSeconds: config.maxAge,
            maxEntries: config.maxEntries
          })
        ]
      });
  
  registerRoute(config.pattern, strategy);
});

10.2 缓存检查清单

typescript
const cacheChecklist = {
  '✅ HTTP缓存配置': [
    '静态资源使用强缓存(max-age=31536000)',
    '动态内容使用协商缓存(ETag/Last-Modified)',
    '设置正确的Vary头',
    '配置Cache-Control的所有必要指令'
  ],
  
  '✅ 文件版本控制': [
    '静态资源使用内容哈希命名',
    'HTML文件不缓存或短期缓存',
    '确保更新后缓存能正确失效',
    '使用import maps管理版本'
  ],
  
  '✅ Service Worker': [
    '实现离线降级页面',
    '正确处理更新流程',
    '清理过期缓存',
    '监控缓存使用量'
  ],
  
  '✅ 性能监控': [
    '跟踪缓存命中率',
    '监控缓存大小',
    '分析缓存效果',
    '设置性能预算'
  ],
  
  '✅ 用户体验': [
    '首次访问体验优化',
    '离线可用性',
    '更新提示',
    '降级方案'
  ]
};

10.3 常见问题解决

typescript
// 问题1: 缓存不生效
// 原因: Cache-Control被覆盖
// 解决: 检查响应头优先级
app.use((req, res, next) => {
  // 确保不被后续中间件覆盖
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
  next();
});

// 问题2: 更新后用户看到旧版本
// 原因: 强缓存未失效
// 解决: 使用文件哈希
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'js/[name]-[hash].js',
        chunkFileNames: 'js/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]'
      }
    }
  }
});

// 问题3: Service Worker不更新
// 原因: 浏览器缓存了SW文件
// 解决: SW文件禁用缓存
app.get('/sw.js', (req, res) => {
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.sendFile('sw.js');
});

// 问题4: 缓存占用过多空间
// 原因: 没有限制缓存大小
// 解决: 设置缓存策略
new ExpirationPlugin({
  maxEntries: 100,
  maxAgeSeconds: 30 * 24 * 60 * 60,
  purgeOnQuotaError: true // 配额不足时自动清理
});

第十一部分:实战案例

11.1 SPA应用缓存优化

typescript
// 优化前: 无缓存策略
// 每次访问都重新下载所有资源
// 首屏加载: 3.5秒

// 优化后: 完整缓存方案
// 1. 构建配置
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['react', 'react-dom'],
          'router': ['react-router-dom'],
          'ui': ['@mui/material']
        }
      }
    }
  }
});

// 2. Service Worker
// sw.js
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then(cache => {
      return cache.addAll([
        '/',
        '/index.html',
        '/manifest.json',
        '/offline.html'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request).then(fetchResponse => {
        return caches.open(DYNAMIC_CACHE).then(cache => {
          cache.put(event.request, fetchResponse.clone());
          return fetchResponse;
        });
      });
    }).catch(() => caches.match('/offline.html'))
  );
});

// 结果:
// 首次访问: 3.5秒
// 再次访问: 0.5秒 (↓85.7%)
// 离线可用: ✅
// 缓存命中率: 92%

总结

本章全面介绍了缓存策略:

  1. 缓存基础 - 理解缓存层级和类型
  2. HTTP缓存 - Cache-Control、ETag等
  3. 构建配置 - Vite文件hash策略
  4. 服务端配置 - Nginx/Node.js缓存
  5. Service Worker - 离线缓存策略
  6. 缓存更新 - 版本控制和失效策略
  7. 最佳实践 - 完整的缓存方案
  8. 性能监控 - 缓存命中率分析
  9. 高级策略 - 智能预缓存、分层缓存
  10. 实战案例 - 真实项目优化经验

合理的缓存策略能够显著提升应用性能和用户体验。

扩展阅读