在做页面性能优化的时候,有一个点容易被忽略,那就是资源缓存优化。 浏览器里缓存策略分为强缓存和协商缓存,每个缓存策略都有其适用的优化场景。 下面为大家详解何为强缓存,协商缓存
先说结论强缓>协商>不缓存。
强缓存
顾名思义,强制缓存,也就是说我的资源请求一次之后,接下来的时间段里如果请求头命中了则直接返回缓存里的资源。通常我们可以在 devtools network 里看到诸如:

from (disk or memory) cache 这样的描述时,这个资源就是走的强缓存,我们可以查看请求头里的字段

这里有两个属性要注意,一个是Expires,还有一个是Cache-Control。
Expires
expires 是 http1.0 的标准,意思在这个时间段里资源都是有效的,超过这个时间则过期需要重新请求。所以我们在请求时如果命中了 expires 则不会发起请求而是直接返回缓存(内存或者磁盘,对应的 Memory,Disk)里的资源,没有命中则重新发起请求。 它是一个绝对值,所以我们可以修改本地的时间来让资源一直缓存或者直接失效。 正是因为这个不安全的因素,http1.0 之后新增了 Cache-Control 字段来作为命中缓存的依据。
Cache Control
与 expires 不同 cache-control 返回的是一个相对值,相对于资源缓存到服务器的时间。在这个有效期内则不会发起请求而是直接返回缓存,超出同样是发起请求来重新获取资源。
需要注意的是强缓存返回的状态码始终都是 200,这点我们可以通过查看 network 里的大小这一栏来判断是否是强缓存,进而通过请求头来判断当前的缓存策略。 同时,上面我说的结论强缓大于协商的原因是,一旦走了强缓就不会再发起请求,所以自然而然就不会再命中协商缓存了。
协商缓存
同样从名字里我们可以知道,协商协商,肯定是客户端与服务器协商后决定究竟是走缓存呐还是重新发起请求。 协商缓存命中的判断依据是:
- 首次请求:
- 会在请求头上带上
Last-Modified或者是ETag
- 会在请求头上带上
- 第二次请求时:
- 判断请求头里的
if-modified-since是否与Last-Modified相同或者是if-none-match===ETag,一旦有一个条件能达成则直接走缓存,否则就重新发起请求。
- 判断请求头里的
协商缓存会先在服务器里判断究竟是要走缓存还是重新发起请求,命中了协商缓存一般请求的状态码是 304

点开请求头我们可以看到if-modified-since和if-none-match

通过对比上一次发起的协商请求发现他们是一样的,这样就会命中协商缓存策略。
不缓存
就是不缓存资源,每次请求都拿最新的。 好处是不会被缓存资源困住,坏处是不缓存了每次都请求很耗服务器的性能。
下面通过koa来实现这 3 个不同的缓存策略机制,新建项目直接npm init -y,然后安装 Koa 和 Koa router
import Koa from "koa";
import Router from "koa-router";
import fs from "node:fs";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = new Koa();
const filePath = path.join(__dirname, "example.txt");
const router = new Router();
router.get("/strong-cache", async (ctx) => {
const stats = fs.statSync(filePath);
const fileContent = fs.readFileSync(filePath, "utf-8");
// 缓存60s
ctx.set("Cache-Control", "public, max-age=60");
ctx.set("Expires", new Date(Date.now() + 60 * 1000).toUTCString());
ctx.type = "text/plain";
ctx.body = fileContent;
});
router.get("/negotiation-cache", async (ctx) => {
const stats = fs.statSync(filePath);
const fileContent = fs.readFileSync(filePath, "utf-8");
const lastModified = stats.mtime.toUTCString();
const etag = `"${stats.size}-${Date.parse(stats.mtime)}"`;
ctx.set("Cache-Control", "no-cache");
ctx.set("Last-Modified", lastModified);
ctx.set("ETag", etag);
const ifModifiedSince = ctx.headers["if-modified-since"];
const ifNoneMatch = ctx.headers["if-none-match"];
if (ifModifiedSince === lastModified || ifNoneMatch === etag) {
ctx.status = 304;
return;
}
ctx.type = "text/plain";
ctx.body = fileContent;
});
router.get("/no-cache", async (ctx) => {
const fileContent = fs.readFileSync(filePath, "utf-8");
ctx.set("Cache-control", "no-store");
ctx.set("Pragma", "no-store");
ctx.type = "text/plain";
ctx.body = fileContent;
});
app.use(router.allowedMethods()).use(router.routes());
app.listen(2000, () => {
console.log("service run on http://localhost:2000");
});
运行这个服务,打开浏览器 然后在开发者工具 devtools 里的 console 里发起 fetch 请求测试
fetch("/strong-cache")
.then((res) => res.text())
.then((res) => console.log(res));
fetch("/negotiation-cache")
.then((res) => res.text())
.then((res) => console.log(res));
然后再观察 Network 请求的情况即可。