🏠 首页 攻略 HTTP缓存策略实战:Cache-Control、ETag和CDN加速全解

HTTP缓存策略实战:Cache-Control、ETag和CDN加速全解

网站加载慢?90%的原因是缓存没配好。本文从HTTP缓存头讲到CDN配置,覆盖Cache-Control、ETag、Service Worker三种策略,附Nginx和Cloudflare实战配置,让你的页面加载速度提升3倍以上。

你的网站为什么加载慢?

打开一个网页,F12看Network面板。你会发现前3秒基本都在等资源下载。

CSS文件50KB,JS文件200KB,图片加起来超过2MB。如果这些资源每次都要重新下载,你的网站能不慢吗?

HTTP缓存是解决这个问题的核心机制。配得好,页面第二次加载只需几百毫秒。配不好,每次都是完整的网络请求。

HTTP缓存的基本原理

浏览器缓存分两层:

强缓存(不走网络请求):浏览器根据Cache-Control头部决定是否直接使用本地副本。命中强缓存时,Network面板显示(memory cache)(disk cache),状态码为200但不会发送HTTP请求。

协商缓存(发请求但可能不传数据):浏览器带上ETag或Last-Modified头向服务器验证资源是否变化。服务器返回304表示"没变,你用本地的",返回200表示"更新了,给你新的"。

两者的关系是:先检查强缓存,不命中就走协商缓存,协商缓存也不命中才真正下载。

Cache-Control详解

这是最常用的缓存控制头。它的值决定了缓存的行为:

Cache-Control: max-age=31536000, immutable

max-age=31536000表示资源在浏览器中可以缓存一年(31536000秒)。immutable告诉浏览器这一年之内资源永远不会变,连协商缓存都不用发。

常见组合:

场景Cache-Control值说明
静态资源(CSS/JS/图片)public, max-age=31536000, immutable长期缓存,文件名带hash时最安全
HTML页面no-cache不走强缓存,每次都要协商验证
动态APIno-store完全不缓存,每次从服务器拿
半静态数据max-age=60, s-maxage=3600浏览器缓存1分钟,CDN缓存1小时

关键点:max-age只对浏览器生效。如果你想让CDN也参与缓存,需要用s-maxage覆盖。

文件名Hash是缓存的灵魂

很多人配了max-age=31536000,然后发现改了CSS之后用户看不到新样式。原因很简单:浏览器还在用旧的缓存文件。

解决方案是给文件名加hash:

style.a3f5b8.css    ← 内容变了,hash就变了
script.9c2d1e.js
image.logo.png      ← 图片用时间戳或版本号

当内容变化时,文件名也跟着变。浏览器认为这是一个全新的资源,直接下载。旧文件因为没人引用,最终会被浏览器清理掉。

Vite、Webpack这些现代构建工具默认就做了这件事。如果你的项目还没加hash,花10分钟配一下,效果立竿见影。

ETag:精确验证的利器

协商缓存有两种方式:Last-Modified和ETag。

Last-Modified基于时间戳,精度只有1秒。如果文件在1秒内被修改了两次,第一次修改不会被检测到。

ETag基于内容的哈希值。服务器给每个资源生成一个唯一标识符,通常是文件的MD5或SHA1:

ETag: "a3f5b8c9d0e1f2"

浏览器再次请求时带上这个值:

If-None-Match: "a3f5b8c9d0e1f2"

服务器比对后返回304(未修改)或200(已更新,附带新的ETag)。

ETag更精确,但计算成本更高。对于小文件来说差别不大,但对于大体积的响应体,建议用Last-Modified节省服务器CPU。

Nginx缓存配置实战

实际项目中,Nginx是最常见的反向代理服务器。以下是生产环境的缓存配置模板:

server {
    listen 80;
    server_name example.com;
    
    # 静态资源:长期缓存
    location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
        root /var/www/html;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    
    # HTML页面:协商缓存
    location ~* \.html$ {
        root /var/www/html;
        add_header Cache-Control "no-cache, must-revalidate";
        add_header Pragma "no-cache";
    }
    
    # API接口:不缓存
    location /api/ {
        proxy_pass http://backend:3000;
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }
    
    # 媒体文件:短期缓存
    location ~* \.(mp4|webm|ogg)$ {
        root /var/www/html;
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
    }
}

几个要点:

  1. 静态资源用expires 1yimmutable,配合文件名hash使用
  2. HTML用no-cache而不是no-store,因为HTML本身很小,协商缓存的成本可以接受
  3. API用no-store彻底禁止缓存
  4. 媒体文件给一个合理的短期缓存时间

CDN缓存配置

CDN的缓存逻辑和本地缓存类似,但层级更多:

用户 → CDN边缘节点 → 源站

CDN缓存的关键参数是TTL(Time To Live)。源站返回的Cache-Control头会被CDN读取,作为缓存时长的依据。

Cloudflare的配置示例:

  1. 进入Caching → Configuration
  2. 设置默认TTL为1个月
  3. 添加规则:路径包含/api/的请求TTL设为0(不缓存)
  4. 添加规则:路径以.js.css结尾的TTL设为1年

更高级的做法是用CDN的"缓存键"功能。默认情况下CDN只按URL做缓存。如果你的页面有动态参数(比如?token=xxx),每个不同的token都会生成一个新的缓存条目,浪费CDN空间。

设置缓存键为只包含域名和路径,忽略查询参数,就能解决这个问题。

Service Worker:客户端级缓存

对于需要离线使用的场景(比如PWA应用),HTTP缓存不够用了。Service Worker能在浏览器层面接管网络请求,实现更精细的控制。

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        // 先查缓存
        if (cachedResponse) {
          // 后台更新缓存
          fetch(event.request).then(networkResponse => {
            if (networkResponse) {
              caches.open('v1').then(cache => {
                cache.put(event.request, networkResponse);
              });
            }
          });
          return cachedResponse;
        }
        // 缓存没命中,走网络
        return fetch(event.request);
      })
  );
});

这个策略叫"缓存优先,后台更新"。用户请求资源时先用缓存,同时后台悄悄拉取最新版本更新缓存。体验上是最快的,数据是最新的。

Service Worker适合SPA应用。对于传统多页网站,HTTP缓存+CDN的组合已经足够好,不需要引入额外的复杂度。

常见缓存陷阱

陷阱一:改了配置但浏览器还在用旧缓存

Chrome DevTools的Network面板有个"Disable cache"复选框。勾选后,只要DevTools开着就不会使用缓存。很多人以为自己的缓存配置无效,其实只是被DevTools欺骗了。关掉这个选项再测。

陷阱二:CDN缓存了不该缓存的内容

动态API返回了max-age=3600,CDN就把结果缓存了一小时。一小时内的所有请求都拿到旧数据。解决方法是在API响应头中加private而不是public,CDN就不会缓存了。

陷阱三:大文件缓存策略不当

视频文件动辄几百MB。如果设置immutable一年,CDN边缘节点会永久存储这些大文件,占用大量带宽和存储。视频文件应该用短TTL(比如1天)加上Range请求支持,让用户断点续传。

用工具验证你的缓存配置

配完缓存之后,怎么确认生效了?

方法一:打开浏览器开发者工具,Network面板勾选Preserve log,刷新页面。看资源的状态码:

  • 显示(memory cache)(disk cache)且无HTTP请求 → 强缓存命中
  • 显示304 Not Modified → 协商缓存命中
  • 显示200 OK且Size显示from disk cache → 首次加载后的磁盘缓存

方法二:用命令行测试响应头:

curl -I https://example.com/style.css

看返回的Cache-ControlETag是否符合预期。

方法三:用Lighthouse做全链路性能审计。它会告诉你哪些资源可以被缓存,哪些应该用CDN。

总结

HTTP缓存不是配一次就完事的。它需要根据你的资源类型、更新频率和用户场景反复调整。

先从最简单的开始:给静态资源加max-age=31536000, immutable,给API加no-store。然后观察Lighthouse的评分变化。你会发现,同样的服务器带宽,缓存配好之后能扛住3倍的流量。

你的网站现在用的什么缓存策略?有没有踩过缓存的坑?评论区聊聊。