HTTP缓存

174 阅读9分钟

HTTP缓存

HTTP内容协商机制

不同浏览器对HTML标签的解析可能存在差异(例如,谷歌浏览器可能自动识别标签,而其他浏览器不会),这会导致页面渲染不一致。为了确保内容正确性,后端需通过响应头明确告知浏览器返回内容的类型和编码,这就是HTTP的内容协商机制

代码示例与解析
以下代码演示如何根据客户端的请求头有Accept返回JSON或HTML格式数据: b0095b96f4a1e7261ed6ad55af0b030.png

const server = http.createServer((req, res) => {
  const { pathname } = url.parse(req.url);
  if (pathname === '/') {
    const accept = req.headers.accept;
    if (accept.includes('application/json')) {
      // 返回JSON数据
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(responseData));
    } else {
      // 默认返回HTML
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(toHTML(responseData));
    }
  }
});
  • 通过req.headers.accept获取客户端期望的响应类型。
  • 设置Content-Type响应头(如text/html; charset=utf-8),明确内容类型和编码,避免浏览器解析差异。

由此我们明白http存在这种内容协商机制

静态资源的传输

方法一:同步读取文件

if (fs.existsSync(filePath)) { // 检查文件是否存在
  const ext = path.parse(filePath).ext; // 获取文件扩展名
  if (ext === '') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
  } else {
    res.writeHead(200, { 'Content-Type': mime.getType(ext) });
  }
  const data = fs.readFileSync(filePath); // 同步读取文件
  res.end(data); // 返回文件内容
}
  • 缺点

    • 同步操作阻塞主线程,性能低。
    • 一次性读取大文件可能导致内存溢出。

方法二:流式传输(推荐)

npm install mime@3

const mime = require('mime');
const stream = fs.createReadStream(filePath); // 创建可读流
res.writeHead(200, { 'Content-Type': mime.getType(ext) });
stream.pipe(res); // 通过管道流式传输到前端
  • 优势

  • 非阻塞I/O,适合大文件传输。

  • 内存占用低,按需加载数据块。

问题暴露: 即使使用流式传输,频繁刷新页面时仍需重复请求资源,加载速度未显著提升。

突破:(HTTP 缓存)将页面上长时间不更新的资源缓存到浏览器上,下次访问页面时该部分资源直接从缓存中获取,从而减少了网络请求的次数,提高了页面的加载速度。

突破性能瓶颈:HTTP缓存

HTTP强缓存

解决方案
强缓存: 在响应头中设置 Cache-Control 字段, 该字段的值为 max-age=xxx , 表示缓存的有效期, 单位为秒。

代码示例

const { ext } = path.parse(filePath);
res.writeHead(200, {
  'Content-Type': mime.getType(ext),
  'Cache-Control': 'max-age=86400', // 强缓存一天
  'Last-Modified': stat.mtimeMs      // 记录文件最后修改时间
});
const stream = fs.createReadStream(filePath);
stream.pipe(res);
  • 执行逻辑

    1. 首次请求时,浏览器缓存资源并记录Last-Modified时间。
    2. 后续请求在缓存有效期内(86400秒)直接使用本地资源,无需请求服务器。

http缓存也叫浏览器缓存,它是http响应头允许做的操作,又是资源缓存到了浏览器上。

size:memery cache命中缓存

a1e92cc4bee529a3e4b7a844263f47b.png

问题:图片被缓存 index.html没有被缓存, 图片请求是在浏览器加载 html代码过程中发的ajax请求,html这个资源的请求是在浏览器加载url路径时发的。通过浏览器 url 地址栏请求的资源,就算后端响应头设置了Cache-Control,因为请求头中就会自动携带 Cache-Control: max-age=0 字段,后端声明的强缓存无效, 也就意味着这种资源无法被强缓存。

注意:不是说网页不会被强缓存,而是资源访问的方式导致这类资源无法命中强缓存。例如,通过<iframe>标签加载的资源可以被强缓存。至于通过浏览器地址栏直接请求的资源(如HTML),其无法被强缓存的原因在于:虽然这类资源会被浏览器缓存,但浏览器在刷新页面时,默认行为是跳过本地缓存,直接向服务器发起请求(地址栏请求默认携带Cache-Control: max-age=0)。因此,这类资源无法通过强缓存机制生效,但并不意味着它们完全不被缓存。

插曲:缓存的地方

被缓存的资源在浏览器的 cache Storage 中,本质上还是在本地硬盘上。

576817c7c30f79fa76d80d662b5048b.png f167d7b3b1ad2cb2acdd1fc86d37cdc.png

试验:将硬盘上的图片替换,查看浏览器页面图片是否被替换,修改图片资源,名字不变,结果没变。

原因:强缓存时效没过,访问的还是那个被缓存的名为xxx图片的资源

措施Ctrl + F5(Windows)或 Cmd + Shift + R(Mac),强制刷新,替换图片出来,强制刷新导致浏览器所有的请求重新发送而不走缓存,强制刷新浏览器,会清空浏览器的 cache Storage。

问题:当公司修改了资源,又不能要求用户每次打开都强制刷新   

解决:一般这种图片资源不会叫普通的名称,而是xxx+一段哈希值(随机值,只要图片更新了值会自动变化,代码会自动更新)。

协商缓存

问题:如何让浏览器地址栏访问的这份html资源缓存

突破点:req的 stats.mtimeMs(最后修改时间),这份资源虽不能强缓存但依旧被浏览器缓存下来,只是浏览器下一次刷新时依旧不会走缓存里面去取html而是问后端要。我们可以让它继续走协商缓存。

协商缓存: 强缓存对浏览器地址栏访问的资源无效,所以浏览器提供了协商缓存的机制。

1. Last-Modified / If-Modified-Since

const { ext } = path.parse(filePath)
const timeStamp = req.headers['if-modified-since']

let status = 200
if (timeStamp && Number(timeStamp) === stat.mtimeMs) {
  status = 304        // 文件修改时间没变
}

res.writeHead(status, {
  'Content-Type': mime.getType(ext),
  'cache-control': 'max-age=86400', // 强缓存一天
  'last-modified': stat.mtimeMs   // 最后修改的时间
})

if (status === 200) {
  const stream = fs.createReadStream(filePath)  // 创建可读流
  stream.pipe(res)  // 将可读流的数据,通过管道,输出到前端
} else {
  return res.end()    // 文件最后一次修改时间没变,返回空
}

原理: 浏览器第一次访问资源时,响应头中携带 last-modified 字段,值为该资源的最后修改时间,当浏览器接收到响应头后,会在该资源再次被请求时,在请求头中自动携带 if-modified-since 字段,值为last-modified的值,后端检验请求头中的if-modified-since 的值和 last-modified 的值是否一致,如果一致,就返回 304 状态码,浏览器就会从缓存中获取该资源,如果不一致,就返回 200 状态码,浏览器就会重新请求该资源。

缺点

Last-Modified 存在一些弊端:如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致返回相同的资源文件和 200。因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改了文件,那么服务端会认为资源还是命中了,不会返回正确的资源。

当文件被修改后但内容没有变更,last-modified 的值会更新,从而导致该重新重新被请求。

ETag / If-None-Match(推荐)

突破点:根据文章内容生成标识,以此判断是否要走缓存

npm i checksum(这个插件可以用来帮我们检查整个文件里的资源有没有被修改过),加密计算文件的MD5值 ,响应头里使用etag字段,值为加密后的值。

const checksum = require('checksum')   // 计算文件的MD5值

const { ext } = path.parse(filePath)
const ifNoneMatch = req.headers['if-none-match']

let status = 200
if (ifNoneMatch === sum) {   // 如果 ETag 头部和 sum 相同,返回 304
  status = 304
}
checksum.file(filePath, (err, sum) => {   // 识别文件资源,资源变更sum变更
  // console.log(sum);  // 6adb05c9d92089507689a7c2beecfc2d814b67eb

  sum = `"${sum}"`
  res.writeHead(200, {
    'Content-Type': mime.getType(ext),
    'cache-control': 'max-age=86400', // 强缓存一天
    'etag': sum   // eTag 头部,如果资源没有变,返回304
  })

  if (status === 200) {
    const reaStream = fs.createReadStream(filePath)
    reaStream.pipe(res)  // 向前端返回文件
  } else {
    res.end()
  }
})

第一次请求,返回的响应头

7367a7ea85b7b82db6f4d9912d6f6c1.png

刷新后的请求头

3fc387a1ab6dc5b0322c86574efb0f9.png

强制刷新后

b028608ce2d5d036f3e60c3caa1ae8a.png

再刷新

f2bfe144ede890161aeae342166d47f.png

ETag 是服务器响应请求时,返回当前资源的一个唯一标识(由服务器生成)只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发起请求时,会将上一次返回的 ETag 值放到 request header 的 If-None-Match 中,服务器只需要对比客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好的判断该资源相对于客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET/POST 200回报的形式将新的资源(当然也包括了新的 ETag)发送给客户端;如果 ETag 一致,则直接返回 304 知会客户端直接使用本地缓存即可。

  • 优势: 基于文件内容生成唯一哈希(如MD5),精度极高。
  • 缺点:不适合大文件,不适合图片资源。

强缓存与协商缓存的对比

机制优点缺点适用场景
强缓存- 零网络请求,性能最优- 资源更新后用户可能看到旧版本长期不变的静态资源(如图片)
协商缓存- 精确判断资源是否变更- 需额外请求验证,增加延迟频繁更新的动态资源(如HTML)

只用强缓存可以把除url地址栏访问的资源缓存起来,但是资源更新了就无法第一时间让前端获取到,所以还需要协商缓存。 优先级规则

  • 强缓存优先:只要未过期,直接使用本地缓存,不触发协商缓存。
  • 过期后协商:强缓存失效后,浏览器携带If-Modified-SinceIf-None-Match向服务器验证资源是否变更。

代码示例

// 强缓存+协商缓存组合
res.writeHead(200, {
  'Cache-Control': 'max-age=86400', // 强缓存一天
  'ETag': fileHash,                // 内容哈希标识
  'Last-Modified': stat.mtimeMs     // 最后修改时间
});
  • 执行逻辑

    1. 首次请求:返回资源并设置缓存头。
    2. 再次请求:若未过期(max-age有效),直接使用强缓存。
    3. 过期后:携带If-None-Match(ETag值)请求服务器,若未变更,返回304,否则返回新资源。

问题:只要命中了强缓存,就不会走协商缓存, 只有强缓存到期,才会走协商缓存。

办法:为了保证文件资源更新前端能够及时获取到, 一般会在文件名后面加上文件指纹(用内容生成 hash 值), 这样文件指纹就会改变, 从而保证文件资源的更新。

五、总结(强缓存+协商缓存+文件指纹)

强缓存+协商缓存

  • 执行顺序
    强缓存 → 过期后触发协商缓存 → 验证ETagLast-Modified

文件指纹

  • 文件名添加哈希值。
  • 内容变化时哈希值自动更新,确保浏览器获取最新资源。