浏览器缓存机制与 CDN
本节目标
- 掌握「强缓存」与「协商缓存」的区别与流程
- 用一个可运行的 Node 小服务器,实测「304 协商缓存」是怎么发生的
- 理解「带 hash 的文件名」为什么是前端工程的关键
两级缓存:
① 强缓存:不请求服务器,直接用本地缓存
Expires(绝对时间,老)/ Cache-Control(相对时间,推荐)
→ Cache-Control: max-age=31536000 表示一年内直接用
② 协商缓存:还是发请求,服务器说「没变」→ 返回 304,浏览器用本地副本
Last-Modified / If-Modified-Since(秒级,可能不精确)
ETag / If-None-Match(内容指纹,推荐)优先级:Cache-Control > Expires;协商缓存里 ETag 优先于 Last-Modified。
流程:
请求资源
→ 命中断言缓存(Cache-Control 未过期)?→ 直接 200(from cache)
→ 否则发请求带 If-None-Match / If-Modified-Since
→ 服务器 304 → 用本地缓存
→ 服务器 200 + 新资源 → 更新缓存下面这段 Node 代码真实实现了 ETag 协商缓存:第一次返回 200+ETag,第二次带 If-None-Match 请求时返回 304。
// 运行环境:Node.js 14+(仅用内置 http,无需装包)
// 步骤:node brw-l11.js (一个文件搞定:先起服务,再自动连发两次请求)
// 第一次打印 200(带 ETag),第二次打印 304(命中协商缓存)
const http = require('node:http')
const crypto = require('node:crypto')
const DATA = 'hello cache'
const etag = '"' + crypto.createHash('md5').update(DATA).digest('hex') + '"'
const server = http.createServer((req, res) => {
const clientETag = req.headers['if-none-match']
if (clientETag === etag) {
res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'max-age=0' })
return res.end() // 没响应体,浏览器用本地缓存
}
res.writeHead(200, { 'ETag': etag, 'Cache-Control': 'max-age=0' })
res.end(DATA)
})
// 先等服务器真正启动成功,再发起请求(放进 listen 回调,避免「服务还没起来就请求」的竞态)
server.listen(3000, async () => {
console.log('server on http://localhost:3000')
const r1 = await fetch('http://localhost:3000/')
const gotEtag = r1.headers.get('etag')
console.log('第一次:', r1.status, 'ETag=', gotEtag)
const r2 = await fetch('http://localhost:3000/', { headers: { 'If-None-Match': gotEtag } })
console.log('第二次(带ETag):', r2.status, '=> 304 即命中协商缓存')
server.close()
})hash 命名与缓存策略(工程关键):
<!-- 文件名带内容 hash:内容变了 hash 变 → 强制走新文件 -->
<script src="/assets/index-CRw3c4JD.js"></script>静态资源(带 hash):Cache-Control: max-age=31536000, immutable (一年强缓存)
HTML(不带 hash) :Cache-Control: no-cache (每次协商,拿到最新引用)CDN 原理:用户 → 就近边缘节点(DNS 解析到最近 CDN)→ 命中缓存直接返,未命中回源站拉取并缓存。优势:就近省时、边缘扛流量、源站压力小;配合 hash 文件名可「更新即全网生效」。
名词解释
强缓存(Strong / Force Cache):浏览器根本不发起网络请求,直接用本地副本。它的「判定方法」是看响应头 Cache-Control: max-age=N 或 Expires 是否还有效——有效就直接 200(from disk/memory cache)。优点:最快(零网络);缺点:资源更新后用户可能拿到旧的(所以用 hash 文件名破局)。
协商缓存(Conditional / Revalidation Cache):浏览器还是会发请求,但带上「我本地这份的版本标识」(ETag 或 Last-Modified),服务器判断「没变」就回 304(空体),浏览器继续用本地。方法:If-None-Match: <etag>(问「和这个指纹一样吗」)、If-Modified-Since: <时间>。它是「强缓存过期后」的第二道防线。
CDN(内容分发网络,Content Delivery Network):把内容缓存到离用户近的「边缘节点」的网络。方法/能力:就近解析(DNS 把用户导向最近节点)、边缘缓存(节点存副本)、回源(未命中时去源站取)。你可以把它想成「在家门口开了个便利店,不用每次都跑总仓」。
课后练习
练习 1:你的网站把 app.js 设了 Cache-Control: max-age=3600,一小时后你改了代码重新部署,但用户反馈「还是旧逻辑」。可能原因和解决办法?
答案:强缓存一小时内未过期,浏览器直接用本地旧
app.js,不请求服务器,所以拿到旧的。若文件名不带 hash,用户得等一小时才更新。正确做法:构建时给文件名加内容 hash(app.ab12cd.js),改代码 hash 变 → URL 变 → 浏览器当新资源重新请求;同时 HTML 用no-cache保证每次拿到最新引用。
练习 2:ETag 和 Last-Modified 都能做协商,为什么更推荐 ETag?
答案:①
Last-Modified精度到「秒」,1 秒内多次改动会误判为未变;② 某些内容没变但修改时间变了(如定时重新生成),Last-Modified会误判为变;③ETag是「内容指纹」(如 md5),内容一字未改就命中,最准确。所以规范推荐以ETag为准(两者可共存,ETag 优先)。
总结
缓存是前端性能里「投入产出比最高」的一环,但也是最容易被误解的一环。我的观点是:很多人把缓存当成「设个 max-age 就完事」,却没理解「强缓存和协商缓存是两套独立机制,且强缓存会让更新『失联』」。真正稳健的工程做法是「带 hash 的资源用一年强缓存,HTML 用 no-cache 每次协商」——让「不变的东西永不重下,变的东西永远最新」。CDN 则是把这个思路放大到地理尺度:把副本放到用户家门口。学完这节你应建立的直觉是:缓存不是「要不要」,而是「哪类资源用哪种策略」,这是每个上线项目都绕不开的硬功夫。