引言:HTTP缓存的重要性

在现代Web开发中,网站性能优化是一个永恒的话题,而HTTP缓存策略是其中最有效且成本最低的优化手段之一。通过合理配置HTTP缓存,我们可以显著减少网络传输数据量、降低服务器负载、加快页面加载速度,从而为用户提供流畅的浏览体验。

HTTP缓存的核心原理是将之前请求过的资源存储在客户端(浏览器)或中间代理服务器中,当再次请求相同资源时,可以直接从缓存中获取,而无需重新从源服务器下载。这种机制不仅能节省用户的带宽,还能大幅减少页面渲染时间。

HTTP缓存的基本概念

缓存的分类

HTTP缓存主要分为两类:

  1. 浏览器缓存:存储在用户本地计算机的内存或磁盘中
  2. 代理服务器缓存:存储在网络路径中的代理服务器上

缓存的工作流程

当浏览器发起HTTP请求时,会按照以下流程检查缓存:

  1. 查找本地是否有该资源的缓存副本
  2. 检查缓存是否过期或失效
  3. 如果缓存有效,直接使用;如果无效,则向服务器发起请求
  4. 服务器返回资源后,根据响应头决定是否更新缓存

强缓存策略

强缓存是浏览器加载资源时最先检查的缓存机制。如果强缓存生效,浏览器不会向服务器发送任何请求,直接从缓存中读取资源。

Cache-Control头部

Cache-Control是HTTP/1.1中最重要的缓存头部,它通过指令组合来控制缓存行为。常用指令包括:

  • max-age=<seconds>:指定资源的最大缓存时间(秒)
  • no-cache:不使用强缓存,但可以使用协商缓存
  • no-store:禁止任何缓存
  • public:资源可以被任何缓存服务器缓存
  • private:资源只能被用户浏览器缓存

示例配置

Cache-Control: max-age=31536000, public

这表示资源可以被任何缓存服务器缓存1年(31536000秒)。

Expires头部

Expires是HTTP/1.0时代的产物,指定资源的过期时间(GMT格式)。

示例

Expires: Wed, 21 Oct 2025 07:28:00 GMT

注意:由于客户端和服务器时间可能不同步,现代应用更倾向于使用Cache-Controlmax-age指令。

实际应用示例

假设我们有一个静态资源服务器,需要对不同类型的文件设置不同的缓存策略:

# Nginx配置示例
server {
    listen 80;
    server_name example.com;
    
    # HTML文件设置较短缓存,便于及时更新
    location ~* \.html$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }
    
    # 图片资源设置长期缓存
    location ~* \.(jpg|jpeg|png|gif|webp)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    
    # CSS/JS文件设置1年缓存,配合文件hash
    location ~* \.(css|js)$ {
        add_header Cache-Control "public, max-age=31536000";
    }
}

协商缓存策略

当强缓存过期或未配置时,浏览器会与服务器进行协商,询问资源是否更新。协商缓存通过条件请求实现,主要涉及以下头部:

Last-Modified / If-Modified-Since

工作原理

  1. 服务器通过Last-Modified头部告知浏览器资源的最后修改时间
  2. 浏览器下次请求时,通过If-Modified-Since头部将该时间发送给服务器
  3. 服务器比较时间,如果资源未修改,返回304状态码;否则返回200和新资源

示例

# 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

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

# 服务器响应(未修改)
HTTP/1.1 304 Not Modified

局限性

  • 只能精确到秒,对于频繁修改的文件可能不准确
  • 如果文件内容未变但修改时间变了(如编辑器保存),会误判为已修改

ETag / If-None-Match

工作原理

  1. 服务器通过ETag头部返回资源的唯一标识(通常是内容的哈希值)
  2. 浏览器下次请求时,通过If-None-Match头部将该标识发送给服务器
  3. 服务器比较ETag,如果相同返回304,否则返回200和新资源

示例

# 首次响应
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

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

# 服务器响应(未修改)
HTTP/1.1 304 Not Modified

优势

  • 基于内容生成,只要内容不变,ETag就不变
  • 比时间戳更精确可靠

缓存策略的实现方式

服务器端配置

Nginx配置

# 完整的缓存配置示例
server {
    listen 80;
    server_name example.com;
    
    # 静态资源目录
    root /var/www/html;
    
    # HTML文件 - 禁止缓存
    location ~* \.html$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }
    
    # 版本化的静态资源(如 app.v123.js)
    location ~* \.[a-z0-9]{8}\.(js|css)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    
    # 普通静态资源
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        # 开启ETag
        etag on;
        
        # 设置缓存时间
        add_header Cache-Control "public, max-age=31536000";
        
        # 对跨域资源设置Access-Control-Allow-Origin
        if ($request_filename ~* \.(woff2|ttf|eot)$) {
            add_header Access-Control-Allow-Origin *;
        }
    }
    
    # API接口 - 协商缓存
    location /api/ {
        # 根据请求参数生成ETag
        set $etag_value "";
        if ($arg_id) {
            set $etag_value "api-$arg_id";
        }
        
        add_header ETag $etag_value;
        add_header Cache-Control "no-cache";
    }
}

Apache配置

<IfModule mod_expires.c>
    ExpiresActive On
    
    # 默认缓存1小时
    ExpiresDefault "access plus 1 hour"
    
    # 图片资源
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    
    # 字体文件
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType font/ttf "access plus 1 year"
    ExpiresByType font/eot "access plus 1 year"
    
    # CSS/JS
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
    
    # HTML
    ExpiresByType text/html "access plus 0 seconds"
</IfModule>

<IfModule mod_headers.c>
    # 为版本化文件设置immutable缓存
    <FilesMatch "\.[a-z0-9]{8}\.(js|css)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

应用层实现

Node.js/Express

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

const app = express();

// 计算文件ETag的辅助函数
function calculateETag(filePath) {
    const fileBuffer = fs.readFileSync(filePath);
    return crypto.createHash('md5').update(fileBuffer).digest('hex');
}

// 静态资源中间件(增强版)
app.use('/static', (req, res, next) => {
    const filePath = path.join(__dirname, 'static', req.path);
    
    // 检查文件是否存在
    if (!fs.existsSync(filePath)) {
        return next();
    }
    
    const stats = fs.statSync(filePath);
    const ext = path.extname(filePath);
    const basename = path.basename(filePath);
    
    // HTML文件 - 禁止缓存
    if (ext === '.html') {
        res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Expires', '0');
        return res.sendFile(filePath);
    }
    
    // 版本化文件(如 app.a1b2c3d4.js)
    const versionPattern = /\.[a-f0-9]{8}\.(js|css)$/;
    if (versionPattern.test(basename)) {
        res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
        return res.sendFile(filePath);
    }
    
    // 其他静态资源
    const etag = calculateETag(filePath);
    const lastModified = stats.mtime.toUTCString();
    
    // 检查协商缓存
    const ifNoneMatch = req.headers['if-none-match'];
    const ifModifiedSince = req.headers['if-modified-since'];
    
    if (ifNoneMatch && ifNoneMatch === etag) {
        res.status(304).end();
        return;
    }
    
    if (ifModifiedSince && ifModifiedSince === lastModified) {
        res.status(304).end();
       	return;
    }
    
    // 设置响应头
    res.setHeader('ETag', etag);
    res.setHeader('Last-Modified', lastModified);
    res.setHeader('Cache-Control', 'public, max-age=31536000');
    
    res.sendFile(filePath);
});

// API接口 - 协商缓存
app.get('/api/user/:id', (req, res) => {
    const userId = req.params.id;
    
    // 模拟从数据库获取数据
    const userData = { id: userId, name: 'John Doe', updated: Date.now() };
    
    // 生成基于用户ID和更新时间的ETag
    const etagValue = `"user-${userId}-${userData.updated}"`;
    
    // 检查客户端ETag
    if (req.headers['if-none-match'] === etagValue) {
        return res.status(304).end();
    }
    
    res.set('ETag', etagValue);
    res.set('Cache-Control', 'no-cache');
    res.json(userData);
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

Python/Django

from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import condition
from django.views.decorators.cache import cache_control
import hashlib
import time

# 使用装饰器设置缓存策略

@cache_control(no_cache=True)
def html_view(request):
    """HTML页面 - 禁止缓存"""
    return render(request, 'page.html')

@cache_control(max_age=31536000, public=True)
def static_resource_view(request, filename):
    """静态资源 - 长期缓存"""
    # 实际项目中应使用Nginx等Web服务器处理静态文件
    return HttpResponse(f"Static content: {filename}")

# 协商缓存示例
def generate_etag_for_user(user_id, timestamp):
    """为用户数据生成ETag"""
    content = f"{user_id}-{timestamp}"
    return hashlib.md5(content.encode()).hexdigest()

def get_user_data(user_id):
    """模拟获取用户数据"""
    # 实际应从数据库获取
    return {
        'id': user_id,
        'name': 'John Doe',
        'updated_at': int(time.time())
    }

@condition(etag_func=lambda req, *args, **kwargs: 
           generate_etag_for_user(kwargs['user_id'], 
                                 get_user_data(kwargs['user_id'])['updated_at']))
def user_api_view(request, user_id):
    """用户API - 支持协商缓存"""
    user_data = get_user_data(user_id)
    return JsonResponse(user_data)

# 更复杂的缓存控制
@cache_control(max_age=3600, private=True)
def private_resource_view(request):
    """私有资源 - 仅浏览器缓存"""
    return HttpResponse("Private resource")

# 使用Cache-Control和ETag组合
def optimized_static_view(request, filename):
    """优化的静态资源视图"""
    response = HttpResponse(f"Static: {filename}")
    
    # 检查文件是否存在并计算ETag
    try:
        # 实际项目中应读取真实文件
        file_content = f"content-{filename}"
        etag = hashlib.md5(file_content.encode()).hexdigest()
        
        # 检查协商缓存
        if request.META.get('HTTP_IF_NONE_MATCH') == etag:
            response.status_code = 304
            response.content = b''
            return response
        
        response['ETag'] = etag
        response['Cache-Control'] = 'public, max-age=31536000'
        
    except FileNotFoundError:
        response.status_code = 404
    
    return response

前端构建工具集成

Webpack配置

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = bundle => {
    // 生成带hash的文件名
    return new (require('html-webpack-plugin'))({
        template: './src/index.html',
        filename: 'index.html',
        // 在HTML中自动引入带hash的资源
        inject: true,
        // 禁止HTML缓存
        cache: false
    });
};

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        // 添加hash到输出文件名
        filename: '[name].[contenthash:8].js',
        chunkFilename: '[name].[contenthash:8].chunk.js',
        clean: true
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            },
            {
                test: /\.(png|jpg|jpeg|gif|svg)$/,
                type: 'asset/resource',
                generator: {
                    // 图片文件名添加hash
                    filename: 'images/[name].[hash:8][ext]'
                }
            }
        ]
    },
    plugins: [
        // 生成带hash的CSS
        new (require('mini-css-extract-plugin'))({
            filename: '[name].[contenthash:8].css'
        }),
        
        // 生成HTML并自动注入资源
        HtmlWebpackPlugin(),
        
        // 清理旧文件
        new (require('clean-webpack-plugin')).CleanWebpackPlugin(),
        
        // 生成manifest文件,便于服务端识别
        new (require('webpack-manifest-plugin')).WebpackManifestPlugin({
            fileName: 'manifest.json',
            generate: (seed, files, entries) => {
                const manifest = {};
                files.forEach(file => {
                    if (file.name && file.path) {
                        manifest[file.name] = file.path;
                    }
                });
                return manifest;
            }
        })
    ],
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                    priority: 10,
                    reuseExistingChunk: true
                },
                common: {
                    name: 'common',
                    minChunks: 2,
                    chunks: 'all',
                    priority: 5,
                    reuseExistingChunk: true
                }
            }
        },
        runtimeChunk: {
            name: 'runtime'
        }
    }
};

Vite配置

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
    plugins: [vue()],
    build: {
        // 输出目录
        outDir: 'dist',
        
        // 生成source map(生产环境可关闭)
        sourcemap: process.env.NODE_ENV === 'development',
        
        // 资源命名规则
        rollupOptions: {
            output: {
                // 入口文件
                entryFileNames: `assets/[name].[hash].js`,
                
                // 代码分割
                chunkFileNames: `assets/[name].[hash].js`,
                
                // 静态资源
                assetFileNames: `assets/[name].[hash].[ext]`
            }
        },
        
        // 压缩配置
        minify: 'terser',
        terserOptions: {
            compress: {
                drop_console: true,
                drop_debugger: true
            }
        }
    },
    
    // 开发服务器配置
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                changeOrigin: true
            }
        }
    }
});

缓存策略的最佳实践

1. 资源分类缓存策略

不同类型的资源应该采用不同的缓存策略:

资源类型 缓存策略 原因
HTML文件 no-cache 或极短时间 内容经常变化,需要及时更新
版本化JS/CSS max-age=31536000, immutable 文件名带hash,内容不变文件名不变
图片/字体 max-age=31536000 变化频率低,长期缓存提升性能
API数据 no-cache 或协商缓存 数据实时性要求高
用户特定内容 private, no-cache 不能被代理服务器缓存

2. 文件名哈希策略

通过在文件名中添加内容哈希,可以实现”永久缓存”:

// 构建输出示例
// app.a1b2c3d4.js - 内容改变时哈希值改变,文件名改变
// app.e5f6g7h8.js - 新版本文件,浏览器视为全新资源

// HTML中引用
<script src="/app.a1b2c3d4.js"></script>
<link rel="stylesheet" href="/style.e5f6g7h8.css">

优势

  • 文件内容不变,哈希不变,文件名不变,缓存永久有效
  • 文件内容改变,哈希改变,文件名改变,浏览器自动加载新文件
  • 无需手动清理缓存

3. 缓存验证与清理

缓存验证工具

# 使用curl验证缓存头
curl -I https://example.com/static/app.a1b2c3d4.js

# 预期输出:
# HTTP/1.1 200 OK
# Cache-Control: public, max-age=31536000, immutable
# ETag: "a1b2c3d4..."
# ...

# 测试协商缓存
curl -I -H "If-None-Match: \"a1b2c3d4...\"" https://example.com/static/app.a1b2c3d4.js

# 预期输出:
# HTTP/1.1 304 Not Modified

缓存清理策略

// Node.js - 手动清理旧缓存
const fs = require('fs');
const path = require('path');

function cleanupOldCache(buildDir, maxAgeDays = 30) {
    const now = Date.now();
    const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
    
    fs.readdirSync(buildDir).forEach(file => {
        const filePath = path.join(buildDir, file);
        const stats = fs.statSync(filePath);
        const fileAge = now - stats.mtimeMs;
        
        if (fileAge > maxAgeMs) {
            console.log(`删除旧文件: ${file}`);
            fs.unlinkSync(filePath);
        }
    });
}

// 在构建后执行
cleanupOldCache('./dist', 30);

4. 缓存监控与分析

// Express中间件 - 记录缓存命中率
function cacheMonitor(req, res, next) {
    const start = Date.now();
    
    // 监听响应完成事件
    res.on('finish', () => {
        const duration = Date.now() - start;
        const cacheStatus = res.statusCode === 304 ? 'HIT' : 'MISS';
        
        console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${cacheStatus} (${duration}ms)`);
        
        // 可以发送到监控系统
        // metrics.increment(`cache.${cacheStatus}`);
    });
    
    next();
}

app.use(cacheMonitor);

常见问题与解决方案

问题1:缓存了不该缓存的资源

症状:更新了HTML文件,但用户看到的还是旧版本。

解决方案

# 确保HTML文件不被缓存
location ~* \.html$ {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

问题2:版本化文件未正确缓存

症状:文件名带hash但未设置immutable,导致每次请求仍验证。

解决方案

# 识别版本化文件并设置immutable
location ~* \.[a-f0-9]{8}\.(js|css)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

问题3:跨域资源缓存问题

症状:CDN资源无法缓存,每次请求都验证。

解决方案

# 服务器设置Access-Control-Allow-Origin
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Cache-Control

# 客户端请求时设置crossorigin
<script src="https://cdn.example.com/lib.js" crossorigin="anonymous"></script>

问题4:移动端缓存空间不足

症状:移动端长期缓存占用过多空间。

解决方案

// 使用Service Worker精细控制缓存
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('v1').then(cache => {
            return cache.addAll([
                '/',
                '/styles/main.css',
                '/scripts/app.js'
            ]);
        })
    );
});

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('v1').then(cache => {
                    cache.put(event.request, responseToCache);
                });
                
                return response;
            });
        })
    );
});

高级缓存策略

Service Worker缓存

Service Worker提供了更精细的缓存控制能力:

// sw.js
const CACHE_NAME = 'app-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 => {
                        cache.put(event.request, responseToCache);
                    });
                    
                    return response;
                });
            })
    );
});

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

CDN缓存策略

# CDN边缘节点配置
server {
    listen 80;
    server_name cdn.example.com;
    
    # 缓存路径
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:100m inactive=60m;
    
    location / {
        # 使用缓存
        proxy_cache my_cache;
        
        # 缓存规则
        proxy_cache_valid 200 302 10m;  # 成功响应缓存10分钟
        proxy_cache_valid 404 1m;       # 404缓存1分钟
        
        # 缓存key(包含请求参数)
        proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";
        
        # 添加缓存状态头(调试用)
        add_header X-Cache-Status $upstream_cache_status;
        
        # 源服务器
        proxy_pass http://backend;
        
        # 缓存控制
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
    }
}

性能测试与监控

缓存命中率监控

// Express中间件 - 缓存统计
const cacheStats = {
    hits: 0,
    misses: 0,
    total: 0
};

function cacheMetrics(req, res, next) {
    const originalEnd = res.end;
    
    res.end = function(...args) {
        cacheStats.total++;
        
        if (res.statusCode === 304) {
            cacheStats.hits++;
        } else if (res.statusCode === 200) {
            cacheStats.misses++;
        }
        
        // 每100个请求打印统计
        if (cacheStats.total % 100 === 0) {
            const hitRate = (cacheStats.hits / cacheStats.total * 100).toFixed(2);
            console.log(`Cache Stats: Hits=${cacheStats.hits}, Misses=${cacheStats.misses}, Hit Rate=${hitRate}%`);
        }
        
        originalEnd.apply(this, args);
    };
    
    next();
}

app.use(cacheMetrics);

Lighthouse性能测试

# 使用Lighthouse测试缓存效果
lighthouse https://example.com --preset=desktop --output=json --output-path=report.json

# 分析报告中的缓存指标
# 查看 "Uses inefficient cache policy" 警告
# 检查 "Serve static assets with an efficient cache policy" 分数

总结

HTTP缓存策略是网站性能优化的基石。通过合理配置强缓存和协商缓存,结合文件名哈希、Service Worker等技术,可以实现:

  1. 显著提升加载速度:减少90%以上的重复资源请求
  2. 降低服务器负载:减少不必要的网络传输和计算
  3. 节省用户带宽:特别对移动端用户意义重大
  4. 改善用户体验:页面响应更快,交互更流畅

关键要点:

  • HTML文件:设置no-cache或极短缓存时间
  • 版本化静态资源:使用max-age=31536000, immutable实现永久缓存
  • 普通静态资源:使用max-age=31536000配合ETag
  • API数据:根据实时性要求选择no-cache或协商缓存
  • 持续监控:关注缓存命中率,及时调整策略

通过本文介绍的原理、实现方式和最佳实践,您可以为网站构建高效的缓存体系,为用户提供极致的性能体验。