彻底搞懂强缓存机制提升页面加载性能
项目初期的技术选型
上个月接手了一个老项目的性能优化任务,页面首屏加载动不动就七八秒,用户反馈说“点开等得都想关了”。打开 DevTools 一看,瀑布流里全是静态资源,JS、CSS、图片,每个都重新请求,ETag 虽然有,但协商缓存还是太慢。
我第一反应是:这不就是强缓存能解决的问题吗?只要给静态资源加个永久 hash 名,配合 Cache-Control: max-age=31536000,浏览器就能直接从 disk cache 拿,根本不用走网络。
听起来简单,实际落地的时候才发现,事情没这么干净。
最大的坑:构建配置和部署流程对不上
我们用的是 Webpack 4,打包后的文件名已经加了 contenthash,比如 app.xxxxx.js。理论上只要配好服务器的 Cache-Control,一切就顺了。我也确实是这么干的:
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
},
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
本地跑起来没问题,build 出来的文件带 hash,HTML 里引用的也是带 hash 的路径。然后我让运维在 Nginx 加了下面这段:
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
结果上线后第二天,产品找我说:“首页样式没了。”
我赶紧去看,发现 HTML 文件被缓存了!而我们的部署方式是每次覆盖 dist 目录,HTML 文件名没 hash,还是 index.html。Nginx 配置一视同仁地把所有静态资源都设了 1 年缓存,包括 HTML。这就完蛋了——用户本地缓存了旧 HTML,里面引用的 JS/CSS 是旧 hash,但服务器上早就删了这些文件,404 了。
这里注意我踩过好几次坑:强缓存必须分对象处理。HTML 不能设长期缓存,因为它控制着资源版本;而 JS/CSS/图片可以而且应该设。
拆开缓存策略,按需配置
后来调整了方案:Nginx 配置改成分开处理。
# 静态资源:强缓存一年,immutable
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存,或最多几分钟
location ~* .html$ {
expires 5m;
add_header Cache-Control "public, must-revalidate";
}
# 如果有 /static/ 这类路径,也可以按目录区分
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
同时在 Webpack 里确保 HTML 输出通过 HtmlWebpackPlugin 生成,并且不被复制到其他无 hash 的路径。
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
inject: 'body'
})
]
这样每次构建,HTML 里的 script 和 link 标签都会自动带上新的 hash 文件名,实现资源层面的版本控制。
CDN 和回源问题又来了
本以为这下稳了,结果过了两天监控报警,部分地区用户加载资源失败。查日志发现,CDN 回源到 origin 时,有些旧文件已经被清理了,但 CDN 上还在返回缓存的内容,而这些内容指向的 JS 文件已经在源站 404。
原因是:我们 CI/CD 流程是 build 后直接 rsync 到服务器,没有保留历史版本。CDN 缓存时间设置的是 1 年,但源站文件被覆盖了。一旦 CDN 节点回源,就 404。
解决方案有两个方向:要么在源站保留所有历史静态文件(不现实),要么让 CDN 缓存时间和源站一致。我们选了后者——在 Nginx 加一个响应头,告诉 CDN 也缓存这么久:
proxy_cache_valid 200 1y;
add_header X-Cache-TTL "31536000";
但这还不够,还得确保部署时不要直接覆盖文件。我们改成每次部署生成一个以 commit hash 命名的目录,比如 /dist/abc123/,然后通过软链切换 current。这样旧版本文件一直存在,直到 CDn 缓存自然过期。
# 部署脚本片段
BUILD_DIR="dist/$(git rev-parse --short HEAD)"
mkdir -p $BUILD_DIR
webpack --mode production
ln -sfn $BUILD_DIR /var/www/html/current
然后 Nginx 指向 /current/ 目录。HTML 文件由后端服务动态输出,路径写成 /current/index.html,但这个 HTML 本身仍然不做强缓存。
最后还有个小问题没彻底解决
现在大部分用户首屏加载都在 1 秒内,强缓存命中率 95% 以上,效果很明显。但有个边缘情况:用户第一次访问是 A 地域的 CDN,第二次访问时到了 B 地域,B 地域的 CDN 节点还没缓存这个资源,会回源。如果此时源站刚好清了旧文件(比如我们设置了 7 天清理策略),就可能 404。
目前的妥协方案是:保留源站静态文件至少 30 天。虽然占点磁盘空间,但比用户报错强。理想方案应该是 CDN + 源站 + 清理策略联动,比如通过 API 通知 CDN 即将下线某个版本,让它提前刷新缓存。但我们团队暂时没精力搞这么重的系统,先这样吧。
回顾与反思
总结一下这次强缓存的实战经验:
- 强缓存不是配个 max-age 就完事的,要区分 HTML 和静态资源
- 构建工具必须输出带 hash 的文件名,否则缓存更新会出问题
- Nginx 配置要精细,别一刀切
- 部署流程必须配合缓存策略,不能随便覆盖文件
- CDN 和源站的缓存生命周期要协调,不然回源 404 很常见
做得好的地方:首屏加载速度提升明显,服务器压力小了,带宽费用也降了。踩过的坑基本都补上了。
还能优化的:自动化清理策略、CDN 缓存预热、更细粒度的缓存头控制。不过目前优先级不高,先放着。
结语
以上是我踩坑后的总结,希望对你有帮助。强缓存听着简单,真落地的时候各种环节都可能翻车。最关键是:别只看浏览器缓存,要看整个链路——构建、部署、服务器、CDN,一个都不能少。
如果有更优的实现方式,欢迎评论区交流。这类问题我估计以后还会遇到,说不定下个项目再写篇续。

暂无评论