出层流量里最重的一类不是接口响应,而是静态资源——图片、脚本、样式表、字体。它们的出层路径不经过业务处理器,却最吃缓存策略:配对了,大部分请求在 304 或本地缓存就结束;配错了,每次刷新都是全量重传。本节把静态服务的装配、强缓存与协商缓存的配方、以及 304 的带宽账一次算清。
第 3.2 节装配过 koa-static,这里把参数语义补全:
const serve = require('koa-static'); const path = require('path'); app.use(serve(path.join(__dirname, 'public'), { maxage: 1000 * 60 * 60 * 24 * 30, // Cache-Control 的 max-age,毫秒单位 immutable: true, // 配合内容哈希文件名,告知期间绝不回源验证 index: 'index.html', // 目录默认文件 gzip: true, // 有同名 .gz 文件时直接出,省实时压缩 }));
三条配置原则来自生产教训。其一,maxage 只配给带哈希的文件。构建工具产出的 app.3f9c2a.js 这类文件名带内容哈希,内容变名就变,配一年强缓存都安全;而 logo.png 这种名字固定的文件,强缓存一年意味着"改了图用户半年看不到"。其二,HTML 不配长缓存——它是所有资源的入口,入口被缓存,新版本资源永远没机会露面,HTML 要么不缓存要么短缓存加协商验证。其三,gzip 选项只对"构建期预压缩"生效,即磁盘上真的存在同名 .gz 文件;没有预压缩流水线就别开,让它去干 koa-compress 的活。
HTTP 缓存是两层的,很多教程混着讲,这里拆开。
强缓存由 Cache-Control 主导:Cache-Control: public, max-age=2592000, immutable 的意思是"有效期之内,浏览器根本不发请求,直接用本地副本"。省的是整个请求。协商缓存由 ETag 或 Last-Modified 主导:缓存过期后,浏览器带着 If-None-Match: "etag值" 来问"这个版本还能用吗",服务端比对后要么回 200 带新内容,要么回 304 Not Modified 不带内容。省的是响应体,请求本身还是要走一趟。
两者的正确关系是接力而不是二选一:强缓存管"新鲜期内别来烦我",协商缓存管"过期之后低成本续命"。Koa 侧开启协商缓存:
const etag = require('koa-etag'); const conditional = require('koa-conditional-get'); app.use(conditional()); // 必须在最外:解析 If-None-Match / If-Modified-Since app.use(etag()); // 为响应生成 ETag,按内容哈希 app.use(serve('./public'));
koa-conditional-get 负责读取请求侧的条件头,koa-etag 负责为出层响应计算并写入 ETag,两者配合,命中时响应自动变成 304。装配顺序 conditional 在 etag 之前,这是它文档里的第一行,也是最容易看漏的一行。

策略值不值,用数字说话。假设一个 200KB 的脚本文件,日请求上万:
账算完结论自明:哈希文件名加强缓存是首选,协商缓存是兜底与补充。HTML 入口单独走短缓存或协商,保证新版本资源"永远能被发现"。
⚠️ 常见坑:缓存策略发布后无法单方面收回。Cache-Control 一旦发出,在 max-age 之内客户端与中间缓存都会忠实执行,你改服务端也改不了用户手里已缓存的副本——这就是"配错 maxage 比不配更糟"的原因。拿不准的新资源先用短 max-age(如十分钟)加协商缓存灰度,观察命中与更新延迟,再逐步拉长。
规模再往上走,静态流量会交给 CDN,此时源站(你的 Koa 服务)的缓存头语义会从"对浏览器说话"变成"对整条缓存链说话"。三条口径要对齐。其一,头就是配置:CDN 大多遵循源站的 Cache-Control——源站说 max-age 一天,边缘节点就缓存一天;你改了源站头,还要确认 CDN 配置没有"忽略源站头"的覆盖项,两边打架时排查会非常痛苦。其二,回源要有保护:缓存大面积失效(新版本发布、CDN 节点抖动)时,海量请求会同时砸回源站——ETag 协商缓存在这里第二次显价值,304 的回源响应几乎不占带宽;更稳的做法是发布前主动预热 CDN,让边缘先填满。其三,动态接口别被误缓存:带 Authorization 头的 API 响应默认不该进共享缓存,必要时显式加 Cache-Control: private, no-store;曾见过网关误把登录用户的响应缓存进边缘节点,造成用户间数据串页的事故——缓存头既是性能工具,也是权限边界。
出层半程到此完整:状态码与头定了骨架、协商与格式填了血肉、静态与缓存管住了最重的流量。下一章视角再拉高一层——当内层的异步出了岔子,错误与进程的稳定性怎么保障。