引言:HTTP缓存的重要性

HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和中间代理服务器中存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。一个正确配置的缓存策略可以将页面加载时间减少50%以上,同时大幅降低带宽成本。

缓存带来的核心优势

  • 减少延迟:避免重复下载相同资源
  • 降低服务器负载:减少不必要的请求处理
  • 提升用户体验:更快的页面响应速度
  • 节省带宽:减少网络传输数据量

HTTP缓存基础概念

缓存的分类

HTTP缓存主要分为两类:

  1. 强缓存(Strong Caching):浏览器直接从缓存中读取资源,无需与服务器通信
  2. 协商缓存(Negotiated Caching):浏览器需要向服务器验证资源是否过期

缓存流程概览

浏览器请求 → 检查缓存 → 强缓存有效? → 是 → 直接使用缓存
                                    ↓ 否
                              发送请求到服务器 → 协商缓存验证 → 资源未修改? → 是 → 304 Not Modified
                                                                        ↓ 否
                                                                  200 OK + 新资源

强缓存策略详解

强缓存通过HTTP响应头控制,当缓存有效时,浏览器不会发送任何网络请求。

Cache-Control头部

Cache-Control是HTTP/1.1中最重要的缓存控制头部,它提供了精细的缓存控制能力。

常用指令详解

max-age=seconds 指定资源的最大缓存时间(秒)。例如:

Cache-Control: max-age=3600

表示资源可以缓存1小时。

no-cache 注意:这个名称容易误解。它不是不缓存,而是必须先与服务器验证缓存有效性。

Cache-Control: no-cache

no-store 真正意义上的不缓存,浏览器和中间缓存都不应存储资源副本。

Cache-Control: no-store

must-revalidate 当缓存过期后,必须重新验证,不能使用过期的缓存。

Cache-Control: must-revalidate

public vs private

  • public:资源可以被任何缓存存储(包括CDN、代理服务器)
  • private:资源只能被用户浏览器缓存,不能被CDN等中间缓存存储

Expires头部(HTTP/1.0)

Expires是HTTP/1.0的遗留头部,指定资源的过期时间(GMT格式)。

Expires: Thu, 31 Dec 2025 23:59:59 GMT

注意Cache-Controlmax-age优先级更高。建议同时设置两者以实现向后兼容。

实际应用示例

静态资源缓存策略

# 静态资源(如JS/CSS/图片)- 长期缓存
Cache-Control: public, max-age=31536000, immutable
Expires: Thu, 31 Dec 2025 23:59:59 GMT

# HTML文档 - 短期缓存或不缓存
Cache-Control: no-cache

协商缓存策略详解

当强缓存过期或未设置时,浏览器会发起请求,通过协商缓存头与服务器确认资源是否需要更新。

基于时间的验证:If-Modified-Since

工作原理

  1. 服务器首次响应时发送Last-Modified头部
  2. 浏览器下次请求时发送If-Modified-Since头部
  3. 服务器比较时间戳,决定返回304还是200

服务器端实现示例(Node.js)

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer((req, res) => {
    const filePath = path.join(__dirname, 'static', req.url);
    
    fs.stat(filePath, (err, stats) => {
        if (err) {
            res.writeHead(404);
            res.end('Not Found');
            return;
        }
        
        const lastModified = stats.mtime.toUTCString();
        const ifModifiedSince = req.headers['if-modified-since'];
        
        // 设置Last-Modified头部
        res.setHeader('Last-Modified', lastModified);
        
        // 比较时间戳
        if (ifModifiedSince === lastModified) {
            res.writeHead(304); // 未修改
            res.end();
        } else {
            // 返回新资源
            res.writeHead(200, {
                'Content-Type': 'text/plain',
                'Cache-Control': 'no-cache'
            });
            fs.createReadStream(filePath).pipe(res);
        }
    });
});

server.listen(3000);

基于内容的验证:ETag

ETag(Entity Tag)是资源的唯一标识符,通常基于内容的哈希值或版本号。

工作原理

  1. 服务器首次响应时发送ETag头部
  2. 浏览器下次请求时发送If-None-Match头部
  3. 服务器比较ETag值,决定返回304还是200

ETag生成示例(Node.js)

const crypto = require('crypto');
const fs = require('fs');

function generateETag(content) {
    return crypto.createHash('md5').update(content).digest('hex');
}

// 在响应中设置ETag
const content = fs.readFileSync('file.js');
const etag = generateETag(content);

res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'no-cache');

// 验证请求
const clientEtag = req.headers['if-none-match'];
if (clientEtag === etag) {
    res.writeHead(304);
    res.end();
} else {
    res.writeHead(200);
    res.end(content);
}

ETag vs Last-Modified 对比

特性 ETag Last-Modified
精确度 基于内容,精确 基于时间,可能因文件属性修改而变化
性能 需要计算哈希,消耗CPU 只需读取文件元数据
可靠性 更可靠 可能因服务器时间不同步而失效
使用场景 频繁修改的资源 静态资源

缓存策略最佳实践

1. 资源分类缓存策略

HTML文档

Cache-Control: no-cache
# 或
Cache-Control: max-age=0, must-revalidate

HTML通常包含动态内容,应设置较短的缓存时间或每次验证。

静态资源(JS/CSS/图片)

# 长期缓存(1年)
Cache-Control: public, max-age=31536000, immutable

# 文件名带哈希值时(如app.a1b2c3.js)
# 可以安全地长期缓存

API响应

# 动态API - 短期缓存
Cache-Control: private, max-age=60

# 不变的API数据
Cache-Control: public, max-age=3600

2. 版本控制与文件哈希

Webpack配置示例

// webpack.config.js
module.exports = {
    output: {
        filename: '[name].[contenthash:8].js',
        chunkFilename: '[name].[contenthash:8].chunk.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            // 自动注入带哈希的资源
            filename: 'index.html'
        })
    ]
};

构建后的文件名

app.a1b2c3d4.js
vendor.e5f6g7h8.js

这样配置后,可以安全地设置长期缓存,因为文件名变化会强制浏览器重新下载。

3. 使用Service Worker进行精细控制

Service Worker提供了程序化的缓存控制能力:

// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
    '/',
    '/styles/main.css',
    '/scripts/app.js',
    '/images/logo.png'
];

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

// 拦截请求并返回缓存
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                // 缓存命中
                if (response) {
                    return response;
                }
                
                // 缓存未命中,发起网络请求
                return fetch(event.request).then(response => {
                    // 检查响应是否有效
                    if (!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }
                    
                    // 克隆响应(响应流只能使用一次)
                    const responseToCache = response.clone();
                    
                    caches.open(CACHE_NAME)
                        .then(cache => {
                            // 只缓存GET请求
                            if (event.request.method === 'GET') {
                                cache.put(event.request, responseToCache);
                            }
                        });
                    
                    return response;
                });
            })
    );
});

// 清理旧缓存
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

4. CDN缓存配置

Nginx配置示例

# 静态资源 - 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header X-Cache-Status $upstream_cache_status;
    
    # 开启gzip压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

# HTML文档 - 不缓存
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
}

# API接口 - 短期缓存
location /api/ {
    expires 60s;
    add_header Cache-Control "private, max-age=60";
}

常见问题与解决方案

问题1:缓存过期导致用户看到旧内容

场景:用户访问网站,看到旧版本的CSS或JS,导致页面显示异常。

解决方案

方案A:文件名哈希(推荐)

// 构建配置
output: {
    filename: '[name].[contenthash:8].js'
}

方案B:版本号查询参数

<!-- 在HTML中引入资源时添加版本号 -->
<script src="/app.js?v=1.2.3"></script>

方案C:清除浏览器缓存

Cache-Control: no-cache, must-revalidate

问题2:缓存穿透

场景:大量请求访问不存在的资源,每次都穿透缓存到数据库。

解决方案

缓存空结果

// Redis缓存示例
async function getFromCache(key) {
    const cached = await redis.get(key);
    if (cached !== null) {
        return JSON.parse(cached);
    }
    
    // 查询数据库
    const data = await db.query(key);
    
    if (data) {
        // 缓存真实数据
        await redis.setex(key, 3600, JSON.stringify(data));
        return data;
    } else {
        // 缓存空结果(设置较短过期时间)
        await redis.setex(key, 60, JSON.stringify({ notFound: true }));
        return null;
    }
}

问题3:缓存雪崩

场景:大量缓存同时过期,导致瞬时请求压垮数据库。

解决方案

随机过期时间

// 设置缓存时添加随机值
function setCacheWithJitter(key, data, baseTTL) {
    // 添加±10%的随机时间
    const jitter = baseTTL * 0.1 * (Math.random() - 0.5);
    const ttl = Math.floor(baseTTL + jitter);
    
    redis.setex(key, ttl, JSON.stringify(data));
}

多级缓存策略

// 本地内存缓存 + Redis缓存
const localCache = new Map();

async function getData(key) {
    // 1. 检查本地缓存
    if (localCache.has(key)) {
        const item = localCache.get(key);
        if (item.expiry > Date.now()) {
            return item.data;
        }
        localCache.delete(key);
    }
    
    // 2. 检查Redis
    const redisData = await redis.get(key);
    if (redisData) {
        // 回填本地缓存
        localCache.set(key, {
            data: JSON.parse(redisData),
            expiry: Date.now() + 30000 // 30秒
        });
        return JSON.parse(redisData);
    }
    
    // 3. 查询数据库
    const dbData = await db.query(key);
    if (dbData) {
        // 同时写入两级缓存
        await redis.setex(key, 3600, JSON.stringify(dbData));
        localCache.set(key, {
            data: dbData,
            expiry: Date.now() + 30000
        });
        return dbData;
    }
    
    return null;
}

问题4:移动端缓存问题

场景:移动端浏览器缓存策略不一致,导致更新后用户仍看到旧内容。

解决方案

移动端专用配置

# 移动端HTML
Cache-Control: no-cache, must-revalidate, max-age=0

# 移动端静态资源
Cache-Control: public, max-age=86400  # 24小时,比桌面端短

检测移动端并动态设置

// Express中间件
app.use((req, res, next) => {
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(req.headers['user-agent']);
    
    if (isMobile) {
        // 移动端使用更保守的缓存策略
        res.setHeader('Cache-Control', 'public, max-age=3600');
    } else {
        // 桌面端使用长期缓存
        res.setHeader('Cache-Control', 'public, max-age=31536000');
    }
    
    next();
});

缓存监控与调试

使用Chrome DevTools调试缓存

  1. Network面板

    • 查看Size列:(memory cache)(disk cache)表示命中缓存
    • 查看Status列:304表示协商缓存命中
    • 查看Time列:缓存命中请求通常<10ms
  2. Application面板

    • 查看Cache Storage中的Service Worker缓存
    • 查看HTTP缓存的使用情况

缓存命中率监控

Node.js监控示例

// Express中间件:记录缓存命中率
const cacheStats = {
    hits: 0,
    misses: 0,
    total: 0
};

app.use((req, res, next) => {
    const originalSend = res.send;
    
    res.send = function(body) {
        const cacheStatus = res.getHeader('X-Cache-Status');
        
        if (cacheStatus === 'HIT') {
            cacheStats.hits++;
        } else if (cacheStatus === 'MISS') {
            cacheStats.misses++;
        }
        
        cacheStats.total++;
        
        // 每1000个请求打印统计
        if (cacheStats.total % 1000 === 0) {
            const hitRate = (cacheStats.hits / cacheStats.total * 100).toFixed(2);
            console.log(`Cache Hit Rate: ${hitRate}% (${cacheStats.hits}/${cacheStats.total})`);
        }
        
        originalSend.call(this, body);
    };
    
    next();
});

// 缓存状态中间件
app.use((req, res, next) => {
    const cacheHeader = req.headers['x-cache-status'];
    
    if (cacheHeader === 'HIT') {
        res.setHeader('X-Cache-Status', 'HIT');
    } else {
        res.setHeader('X-Cache-Status', 'MISS');
    }
    
    next();
});

使用WebPageTest进行性能测试

配置缓存测试

// WebPageTest脚本
{
    "tests": [
        {
            "label": "Cache Performance",
            "url": "https://your-site.com",
            "firstViewOnly": false,
            "repeatView": true,
            "cache": true
        }
    ]
}

高级缓存策略

1. Vary头部的使用

Vary头部用于指定哪些请求头影响缓存的变体。

# 根据User-Agent返回不同内容
Vary: User-Agent

# 根据Accept-Encoding返回不同内容(压缩)
Vary: Accept-Encoding

# 多个请求头
Vary: User-Agent, Accept-Language

注意:谨慎使用Vary: *,这会完全禁用缓存。

2. 预加载和预取

资源预加载

<!-- 在HTML中预加载关键资源 -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/app.js" as="script">
<link rel="preload" href="/hero-image.jpg" as="image">

DNS预取

<link rel="dns-prefetch" href="//cdn.example.com">

3. 缓存破坏(Cache Busting)策略

文件名哈希(最佳实践)

app.a1b2c3d4.js → 内容变化 → app.e5f6g7h8.js

查询参数(次选)

<script src="/app.js?v=1.2.3"></script>

目录版本

<script src="/v1.2.3/app.js"></script>

性能测试与优化

缓存性能指标

关键指标

  • 缓存命中率:>90%为优秀
  • 平均响应时间:缓存命中应<50ms
  • 带宽节省:应减少60-80%的重复数据传输

使用Lighthouse审计缓存

Lighthouse缓存审计规则

{
  "cache-policy": {
    "maxAge": 86400,
    "mustRevalidate": false
  }
}

缓存优化检查清单

  • [ ] 静态资源是否设置了长期缓存?
  • [ ] HTML文档是否设置了合理的缓存策略?
  • [ ] 是否使用文件名哈希避免缓存问题?
  • [ ] 是否启用了CDN缓存?
  • [ ] 是否监控缓存命中率?
  • [ ] 是否处理了缓存穿透和雪崩?
  • [ ] 是否使用了Service Worker进行精细控制?
  • [ ] 是否测试了移动端缓存行为?

总结

HTTP缓存是提升网站性能的关键技术。通过合理配置Cache-ControlETag等头部,结合文件名哈希、CDN、Service Worker等技术,可以显著提升用户体验并降低服务器成本。

核心原则

  1. 静态资源长期缓存:使用文件名哈希+max-age=31536000
  2. HTML文档谨慎缓存:使用no-cache或短max-age
  3. API响应按需缓存:根据数据时效性设置合适的max-age
  4. 监控与调优:持续监控缓存命中率并优化策略

通过本文介绍的策略和实践,您可以构建一个高性能、高可靠性的缓存系统,为用户提供流畅的Web体验。