性能优化简介

缓存策略

掌握 HTTP 强缓存与协商缓存的工作机制,学会为静态资源制定合理的缓存策略。

🎯 引言

用户第一次打开你的网站,浏览器下载了 HTML、JS、CSS、图片等一堆资源。第二次打开时,这些资源还要重新下载一遍吗?如果每次都重新下载,页面加载慢、服务器压力大、用户流量也白白浪费。HTTP 缓存就是来解决这个问题的:把下载过的资源存在浏览器本地,下次直接用。

学完本篇,你将能:

  • 说清楚强缓存和协商缓存分别是什么、如何配合工作。
  • 看懂 Cache-ControlExpiresETagLast-Modified 这些响应头的含义。
  • 理解为什么构建工具要给 JS/CSS 文件名加内容哈希。
  • 为自己的项目制定一套合理的缓存策略。

🧱 为什么缓存很重要

先看一组生活化的对比。你去图书馆借了一本书回家看,第二天想再看一遍,你会直接翻家里的那本,而不是再跑一趟图书馆。浏览器缓存就是「把书借回家」:资源下载过一次之后,存在本地,下次请求时先看本地有没有可用的副本。

没有缓存时,每次访问页面都要重新下载所有资源,网络请求多、等待时间长。有了缓存之后,重复访问时浏览器直接使用本地副本,很多请求根本不需要发出去,页面加载自然更快,服务器的压力也随之减轻。

HTTP 缓存分为两层,按优先级依次生效:

  1. 强缓存:本地副本还没过期,直接用,完全不发请求。
  2. 协商缓存:本地副本过期了,发个请求问服务器「内容变了吗」,没变就继续用本地的,变了才下载新内容。

下面我们分别来看这两层是怎么工作的。


🧱 强缓存:没过期就一言不发

强缓存的核心是一句话:只要缓存没过期,浏览器根本不会向服务器发请求,直接用本地副本。

Expires:旧的方案

Expires 是 HTTP/1.0 时代的响应头,服务器告诉浏览器「这份资源在这个时间点之前是新鲜的」:

HTTP/1.1 200 OK
Content-Type: text/css
Expires: Wed, 21 Jul 2027 02:45:43 GMT

浏览器看到 Expires 后,会记住这个过期时间点。过期之前再次请求这个资源,直接用本地缓存。

这个方案有个明显的缺陷:它依赖用户电脑的本地时间。如果用户的系统时间不准(比如手动调快了),浏览器判断的「是否过期」就是错的。因此它逐渐被更可靠的方案取代,现在主要作为兼容手段出现。

Cache-Control:主流方案

Cache-Control 是 HTTP/1.1 的响应头,用相对时长代替绝对时间点,不再受用户本地时间影响:

HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: max-age=31536000

max-age=31536000 表示:从这次响应开始算,这份资源在 31536000 秒(一年)内都是新鲜的。浏览器拿到响应后记下「收到时间 + max-age」,在有效期内再次请求,直接使用本地缓存,请求压根不会发出去。

两个响应头同时出现时,Cache-Control 的优先级高于 Expires。现代项目里通常只配置 Cache-Control

这里先记住一个结论:Cache-Control 里带有效的 max-age=秒数,就是强缓存。它还有另外两个常见取值,含义正好相反,都是「不使用强缓存」:

  • no-cache:名字容易误导,它不是「不缓存」,而是「不走强缓存」,每次使用前都要先找服务器确认一次。至于怎么确认,正是下一节「协商缓存」要讲的内容。
  • no-store:这才是「完全不缓存」,每次都要重新下载。
在 Chrome DevTools 的 Network 面板中,命中强缓存的请求,Size 列会显示 (disk cache)(memory cache),Status 列显示灰色的 200,这是判断强缓存是否生效的直观办法。
Chrome DevTools 的界面会不定期更新,面板名称和列的位置可能与你看到的略有不同。以上描述仅供参考,核心思路不变,以你实际看到的界面为准。

🧱 协商缓存:过期了就问一句

强缓存到期之后,本地副本不能直接用了,但也不代表内容一定变了。这时进入协商缓存:浏览器带着缓存的「身份证明」去问服务器,内容没变就继续用本地副本,变了才下载新的。

ETag 与 If-None-Match

ETag 是服务器给资源生成的一个标识串,可以理解成资源的「指纹」,内容一变指纹就变。服务器第一次响应时带上它:

HTTP/1.1 200 OK
Content-Type: text/css
ETag: "abc123"

缓存过期后,浏览器再次请求时,会把这个指纹放进 If-None-Match 请求头里一起发过去:

GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "abc123"

服务器对比指纹:内容没变,就返回 304 Not Modified,响应里只有头部、没有正文,浏览器收到后继续用本地缓存:

HTTP/1.1 304 Not Modified
ETag: "abc123"

内容变了,则正常返回 200 和新内容,同时附上新的 ETag。由于 304 响应不携带正文,传输的数据量很小,比重新下载整个文件省得多。

Last-Modified 与 If-Modified-Since

这是另一组协商缓存的搭档,思路是用「修改时间」代替「指纹」。服务器响应时带上资源的修改时间:

HTTP/1.1 200 OK
Content-Type: text/css
Last-Modified: Wed, 21 Jul 2026 02:45:43 GMT

缓存过期后,浏览器带着这个时间去问:

GET /style.css HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 21 Jul 2026 02:45:43 GMT

服务器发现资源在这个时间之后没改过,同样返回 304 Not Modified

ETag 优先于 Last-Modified

两组搭档同时存在时,服务器优先用 ETag 判断。原因是时间的精度不够用:Last-Modified 只能精确到秒,如果文件在一秒内被修改了两次,第二次修改就识别不出来。另外有些文件会被重新保存但内容没变,修改时间更新了,会白白触发一次重新下载,而 ETag 按内容生成指纹,不受这个影响。

整个协商缓存的流程可以概括成一句话:缓存过期后先问一句,没变就 304 继续用,变了才下载新的。


⚡ 强缓存 vs 协商缓存

两种缓存各有分工,用一张表对比它们的差异:

对比项强缓存协商缓存
触发时机缓存未过期时缓存已过期时
是否发请求不发,直接用本地副本发请求,向服务器确认
相关响应头Cache-ControlExpiresETagLast-Modified
相关请求头If-None-MatchIf-Modified-Since
服务器响应无(请求未发出)内容没变返回 304,变了返回 200
传输的数据量0只传响应头(304 时),不传输资源正文
典型状态码200(来自缓存)304 Not Modified

可以看到,强缓存省掉了整个请求,代价是过期前拿不到更新;协商缓存每次都要问一次服务器,但能保证及时拿到新内容。实际项目里两者搭配使用,下一节就是具体怎么搭。


🛠 静态资源的缓存实践

理论讲完了,落到真实项目里,缓存策略该怎么定?关键在于:不同资源要区别对待。

带哈希的文件名:超长缓存的底气

现代构建工具(Webpack、Vite 等)打包 JS/CSS 时,会根据文件内容在文件名里加一段哈希,例如 app.a1b2c3d4.js。内容只要改一个字节,哈希就变,文件名也随之变成 app.e5f6g7h8.js

这个机制带来一个很好用的性质:内容变了,文件名就是全新的,浏览器会把它当成一个新资源去下载,旧缓存自然失效,不需要任何人工干预。

所以对于这类带哈希的文件,可以放心设置超长的 max-age(注意这是服务器的响应头,配置方法见下一节):

Cache-Control: max-age=31536000

一年之内,用户再次访问都直接用本地缓存;而项目一发新版,新文件名的资源又会自动被下载。关于构建工具如何生成哈希文件名,Vite 课程里有更详细的介绍,这里不展开。

HTML 入口文件:不能久放

HTML 是页面的入口,里面引用了带哈希的 JS/CSS 地址。如果把 HTML 也缓存一年,用户拿到的一直是旧 HTML,引用的还是旧哈希的 JS,新版本就发不出去了。

所以 HTML 入口文件要用协商缓存或较短的缓存时间,保证用户能及时拿到新版本:

Cache-Control: no-cache

no-cache 表示每次使用前都向服务器确认一次:没变就 304 用本地的(开销很小),变了就拿新 HTML,进而引用新哈希的 JS/CSS。这样整个更新链路就通了。

用 Express 配置缓存响应头

理解了策略,写代码就不难了。下面是一个精简的 Express 示例(本地验证环境:Node.js v22 + express v4),给带哈希的静态资源设置超长缓存,给 HTML 设置协商缓存:

server.js
const express = require('express');
const app = express();

// 带哈希的静态资源:超长强缓存
app.use(
    '/assets',
    express.static('dist/assets', {
        maxAge: '365d', // 等价于 max-age=31536000
    }),
);

// HTML 入口文件:每次使用前都找服务器协商
app.use(
    express.static('dist', {
        setHeaders(res, filePath) {
            if (filePath.endsWith('.html')) {
                res.setHeader('Cache-Control', 'no-cache');
            }
        },
    }),
);

app.listen(3000);

如果你还不熟悉 Express 的静态资源托管,可以先看 Express 课程里的相关章节,这里只需要关注 maxAgesetHeaders 这两个配置点。

Cache-Control 是服务器返回的响应头,配置在部署项目的服务器上(Nginx、CDN 或 Express 这类服务),前端 JS 代码设置不了它。

🧾 小节总结

  • HTTP 缓存分两层:强缓存(未过期直接用本地副本,不发请求)和协商缓存(过期后问服务器,没变返回 304 继续用)。
  • 强缓存用 Cache-Control: max-age=秒数,是主流方案;Expires 是旧方案,依赖用户本地时间,容易判断失误。
  • 协商缓存靠 ETag / If-None-MatchLast-Modified / If-Modified-Since 两组搭档,ETag 按内容生成指纹,优先级更高,因为修改时间精度只到秒。
  • no-cache 不是不缓存,而是每次使用前都先协商;no-store 才是完全不缓存。
  • 实践策略:带内容哈希的 JS/CSS 设置超长 max-age,内容变了文件名就变、缓存自然失效;HTML 入口文件用 no-cache 走协商缓存,保证新版本及时生效。
  • Cache-Control 是服务器响应头,要在部署的服务器(Nginx、CDN、Express 等)上配置,前端 JS 设置不了。

❓ 知识问答

Q1:强缓存生效时,刷新页面还会走缓存吗?

A:普通刷新(F5)通常仍会命中强缓存。强制刷新(Ctrl/Cmd + Shift + R)会绕过本地缓存,直接向服务器请求所有资源,排查缓存问题时经常用到。

Q2:no-cacheno-store 有什么区别?

A:no-cache 会把资源存下来,但每次使用前都要向服务器确认,相当于直接进入协商缓存。no-store 是完全不存储,每次都要重新下载,常用于包含敏感信息的响应。

Q3:既然有 ETag,为什么还要保留 Last-Modified?

A:Last-Modified 主要作为兜底和兼容手段存在。两组头同时出现时,服务器优先用 ETag 判断;有些场景下(比如只关心粗略的修改时间)Last-Modified 也够用。

Q4:HTML 为什么不设置 max-age=0 而要写 no-cache

A:两者效果接近,都表示每次使用前要向服务器确认。no-cache 的语义更直白,是现代项目的常用写法。

Q5:用户一直拿到旧版本的 JS,可能是什么原因?

A:常见的嫌疑是 HTML 被缓存了:旧 HTML 里引用的是旧哈希的 JS 地址。检查 HTML 响应头是否设置了协商缓存,必要时在 HTML 的响应头里加上 Cache-Control: no-cache

Q6:缓存响应头能在前端 JS 代码里设置吗?

A:不能。响应头是服务器返回资源时带上的,前端 JS 拿不到这个环节,要在部署项目的服务器(Nginx、CDN 等)上配置。


🧪 小练习

练习一:基于上面的 Express 示例,补充一个「图片资源缓存 30 天」的规则,并启动服务后,用 Chrome DevTools 的 Network 面板验证各资源的 Cache-Control 响应头是否符合预期。

server.js
const express = require('express');
const app = express();

app.use(
    express.static('dist', {
        setHeaders(res, filePath) {
            if (filePath.endsWith('.html')) {
                res.setHeader('Cache-Control', 'no-cache');
            }

            // 请在这里编写代码:以 .png / .jpg 结尾的文件,设置 max-age 为 30 天(2592000 秒)
        },
    }),
);

app.listen(3000);

练习二:找一个你经常访问的网站,打开 Chrome DevTools 的 Network 面板刷新页面,观察几个请求的 Status 和 Size 列:找出一条命中强缓存的请求和一条返回 304 的请求,看看它们各自的响应头里带了哪些缓存相关字段。


🎉 恭喜你已经掌握 HTTP 缓存策略啦!现在你不仅知道强缓存和协商缓存怎么工作,还能为不同资源制定「静态资源超长缓存、HTML 走协商」的实用方案。下一篇我们进入构建优化,看看怎么从打包产物本身入手给页面提速。