Appearance
缓存策略
课程概述
本章节深入探讨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
有缓存:
请求 → 本地缓存 → 响应
时间: 5ms1.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, private2.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 OK2.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-pluginjavascript
// 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=3007.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%总结
本章全面介绍了缓存策略:
- 缓存基础 - 理解缓存层级和类型
- HTTP缓存 - Cache-Control、ETag等
- 构建配置 - Vite文件hash策略
- 服务端配置 - Nginx/Node.js缓存
- Service Worker - 离线缓存策略
- 缓存更新 - 版本控制和失效策略
- 最佳实践 - 完整的缓存方案
- 性能监控 - 缓存命中率分析
- 高级策略 - 智能预缓存、分层缓存
- 实战案例 - 真实项目优化经验
合理的缓存策略能够显著提升应用性能和用户体验。