HTTP 缓存详解
打开一个网页时,你有没有想过:为什么第二次打开会比第一次快那么多?答案藏在HTTP 缓存里 —— 它是减少网络请求、节省带宽、提升页面加载速度的核心手段。
今天我们就顺着 HTTP 缓存的逻辑,把 “强制缓存”“协商缓存” 这些概念拆透,再补上实际开发里的实用技巧。
一、为什么 HTTP 缓存是性能优化的 “刚需”?
网页里的静态资源(图片、JS、CSS)、甚至部分重复数据,往往是 “重复请求但内容不变” 的。如果每次打开页面都重新从服务器下载这些资源,不仅会拖慢加载速度,还会浪费服务器带宽。
HTTP 缓存的思路很简单:把 “请求 - 响应” 的数据存到本地,下次直接读本地,不用再发网络请求—— 这也是 HTTP/1.1 性能提升的关键设计之一。
而 HTTP 缓存的实现,主要靠 “强制缓存” 和 “协商缓存” 这两层逻辑。
二、第一关:强制缓存 —— 浏览器说了算的 “本地库存”
什么是强制缓存?
就像你家里囤的零食:只要没过期,你就直接拿出来吃,不用再去超市买。强制缓存就是 “浏览器判断缓存没过期,直接用本地存储的资源”,主动权完全在浏览器这边。
Request URL http://XX.XX.XX.XX:PORT/XXXX/XX/XX.js
Request Method GET
Status Code 200 OK (from disk cache)
Remote Address XX.XX.XX.XX:PORT
Referrer Policy strict-origin-when-cross-origin
比如上面GET请求响应显示的 “200 (from disk cache)”,就是浏览器直接用了本地磁盘里的缓存资源,连网络请求都没发。
核心控制头部:Cache-Control
强制缓存的 “保质期”,是通过响应头里的Cache-Control字段控制的,它有几个常用指令:
max-age=3600:缓存 3600 秒(1 小时)后过期(最常用);public:允许中间缓存(比如 CDN)也存这份资源;private:只有浏览器能存,中间缓存不能存(比如用户专属的资源);no-cache:不是 “不缓存”,是 “需要先协商缓存再用”;no-store:完全不缓存,每次都要从服务器重新请求(比如敏感数据)。
被替代的 “老古董”:Expires
在 HTTP/1.0 里,强制缓存靠Expires字段(比如Expires: Wed, 05 Jan 2026 12:00:00 GMT),它用 “绝对时间” 标记过期时间。
但这个方案有个坑:如果客户端和服务器的时间不同步(比如用户改了本地时间),缓存的过期判断就会出错。所以现在基本用Cache-Control: max-age(相对时间)替代它。
强制缓存的完整流程
- 浏览器第一次请求资源,服务器返回资源时,在响应头加
Cache-Control: max-age=3600; - 浏览器把资源和
Cache-Control一起存到本地; - 1 小时内再次请求这个资源:浏览器直接读本地缓存,返回 “200 (from cache)”,不发网络请求;
- 1 小时后缓存过期,才会进入下一关 “协商缓存”。
三、第二关:协商缓存 —— 服务器拍板的 “库存核验”
如果强制缓存过期了,是不是就得重新下载资源?不一定 —— 这时候会进入 “协商缓存”:浏览器带着缓存的 “标识” 问服务器 “这个资源过期了,你看看现在能用不?”,服务器判断后决定是用缓存还是发新资源。
协商缓存的标志是响应码304 Not Modified—— 意思是 “资源没变化,你用本地缓存就行”。
协商缓存的两种 “标识方案”
服务器和浏览器靠两个头部组合来判断资源是否变化:
方案 1:Last-Modified + If-Modified-Since(基于 “修改时间”)
- 第一次请求:服务器在响应头加
Last-Modified: Wed, 03 Jan 2026 10:00:00 GMT(资源最后修改时间); - 缓存过期后请求:浏览器在请求头加
If-Modified-Since: Wed, 03 Jan 2026 10:00:00 GMT(把上次的 Last-Modified 带过去); - 服务器对比:如果资源现在的修改时间和这个时间一样,返回
304;如果不一样,返回200+ 新资源 + 新的 Last-Modified。
方案 2:Etag + If-None-Match(基于 “唯一标识”)
Last-Modified有几个天生的坑:
- 资源内容没改,但修改时间变了(比如服务器重启),会被误判为 “资源更新”;
- 时间精度是秒级,如果资源在 1 秒内被多次修改,无法识别;
- 有些服务器拿不到资源的修改时间。
所以有了更可靠的Etag:服务器给每个资源生成一个唯一标识(比如哈希值),资源内容变了,Etag 就会变。
流程和上面类似:
- 第一次请求:服务器响应头加
Etag: "abc123"; - 缓存过期后请求:浏览器请求头加
If-None-Match: "abc123"; - 服务器对比:Etag 一样返回
304,不一样返回200+ 新资源 + 新 Etag。
优先级:Etag > Last-Modified
如果服务器同时返回了Etag和Last-Modified,浏览器会优先用Etag做协商 —— 毕竟它能更准确地判断资源是否真的变化。
四、补充:缓存存在哪?内存 VS 磁盘
浏览器的缓存会存在两个地方:
- 内存缓存(from memory cache):存在内存里,读取速度极快,但关闭标签页就会被清空(适合小体积、常用的资源,比如 JS/CSS);
- 磁盘缓存(from disk cache):存在本地磁盘里,读取速度稍慢但持久化(适合大体积资源,比如图片、视频)。
你在浏览器开发者工具里看到的 “from memory cache”“from disk cache”,就是缓存的存储位置标识。
五、开发必看:HTTP 缓存的最佳实践
- 静态资源加 “哈希后缀”:比如把
app.js改成app.abc123.js—— 资源更新时哈希会变,浏览器会自动请求新资源,不用怕缓存失效不及时; - 合理设置
max-age:静态资源(图片、JS/CSS)设长一点(比如max-age=31536000,即 1 年),因为哈希变了会自动更新; - 动态接口用
no-cache:比如用户信息接口,加Cache-Control: no-cache—— 强制走协商缓存,既利用缓存又保证数据新鲜; - 配合 CDN 缓存:把静态资源放到 CDN 上,CDN 会帮你缓存资源,进一步减少源站压力。
总结
HTTP 缓存的核心逻辑是 “强制缓存优先,协商缓存兜底”:
- 强制缓存没过期:浏览器直接用本地资源,零请求成本;
- 强制缓存过期:通过协商缓存让服务器判断,能复用就返回 304,不能复用再发新资源。
合理利用 HTTP 缓存,能让你的页面加载速度 “起飞”—— 毕竟少发一次请求,就多省一点时间。