技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

前端也要懂 HTTP 缓存机制

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/17-前端也要懂Http缓存机制
本文目录11 个章节
  1. 一、HTTP 请求和响应
  2. 二、HTTP 缓存的基本分类
  3. 三、先运行一个没有显式缓存策略的服务器
  4. 四、强缓存实验
  5. 五、协商缓存实验
  6. 六、Pragma:保留它的历史位置
  7. 七、如何观察实验结果?
  8. 八、实际项目的缓存策略
  9. 九、原文中需要修正的字段和结论
  10. 总结
  11. 参考资料

前端也要懂 HTTP 缓存机制

Category(分类): Browser, HTTP Cache Status: 已更新

原文通过 Express 手动添加响应头,观察 ExpiresCache-ControlLast-ModifiedETag 的效果。这个实验方法很适合学习,本文保留实验路线和示意图,并修正原文中的字段拼写、缓存优先级和“每次都会重新请求”等不准确表述。

一、HTTP 请求和响应

浏览器与服务器通过 HTTP 请求-响应进行通信。请求和响应都由起始行、头部字段和可选消息内容组成:

HTTP 请求响应模型

GET /assets/demo.js HTTP/1.1
Host: localhost:8080
Accept: */*
HTTP/1.1 200 OK
Content-Type: text/javascript; charset=utf-8
Cache-Control: no-cache
ETag: "demo-v1"

console.log('demo')

与缓存相关的常见字段:

字段方向作用
Cache-Control请求/响应控制缓存新鲜度、验证和存储策略
Expires响应HTTP/1.0 风格的绝对过期时间
Last-Modified响应资源最近修改时间
If-Modified-Since请求使用上次的 Last-Modified 进行条件验证
ETag响应服务器生成的实体标签
If-None-Match请求使用上次的 ETag 进行条件验证
Vary响应声明哪些请求头会影响缓存变体
Pragma请求/历史响应HTTP/1.0 时代的兼容字段,现代项目不应依赖它控制全部缓存

二、HTTP 缓存的基本分类

HTTP 标准更常用“私有缓存”和“共享缓存”描述缓存位置:

  • 私有缓存:通常是用户浏览器自己的缓存,可以保存个性化响应;
  • 共享缓存:代理、CDN、反向代理等可能为多个用户复用响应,不能保存未隔离的个性化内容。

前端资料常把行为分为:

  • 强缓存:响应仍 fresh 时直接复用,不访问源站;
  • 协商缓存:响应需要验证时发送条件请求,未变化返回 304

这是学习模型,不是浏览器内部唯一的固定算法。Service Worker、Cache API、BFCache、缓存分区、重定向、请求 cache 模式和 Vary 都可能改变最终结果。

三、先运行一个没有显式缓存策略的服务器

const express = require('express')
const fs = require('node:fs')
const path = require('node:path')

const app = express()
const port = 8080
const filePath = path.join(__dirname, 'static', 'demo.js')

app.get('/', (req, res) => {
  res.type('html').send(`<!doctype html>
<html lang="zh-CN">
  <head><meta charset="utf-8"><title>HTTP Cache Demo</title></head>
  <body>
    <p>HTTP Cache Demo</p>
    <script src="/demo.js"></script>
  </body>
</html>`)
})

app.get('/demo.js', (req, res, next) => {
  fs.readFile(filePath, (error, content) => {
    if (error) return next(error)
    return res.type('js').send(content)
  })
})

app.listen(port, () => {
  console.log(`http://localhost:${port}`)
})

未设置缓存头时的请求示意

没有显式 Cache-Control 时,HTTP 规范允许缓存根据启发式规则存储和复用某些响应;浏览器还可能因为刷新、开发者工具设置、请求上下文和缓存策略重新验证。因此不能绝对说“第二次访问一定重新读取磁盘”,也不能只凭代码判断浏览器是否命中缓存,应以 Network 面板和响应头为准。

生产项目通常直接使用 express.static,它可以提供 ETag 和 Last-Modified;是否设置长缓存应根据 URL 是否内容指纹化决定。

四、强缓存实验

1. Expires

Expires 使用 GMT 绝对时间:

app.get('/expires.js', (req, res, next) => {
  fs.readFile(filePath, (error, content) => {
    if (error) return next(error)

    const expiresAt = new Date(Date.now() + 2 * 60 * 1000)
    res.set('Expires', expiresAt.toUTCString())
    return res.type('js').send(content)
  })
})

Expires 强缓存示意

如果当前响应仍然 fresh,浏览器可能直接从 memory cache 或 disk cache 读取。两分钟后,缓存变陈旧,浏览器可能重新请求或进行协商验证,具体还取决于是否存在其他验证器。

Expires 的缺点是依赖绝对时间,客户端和服务器时钟不一致可能造成判断偏差。它并没有“失效”,只是现代 HTTP/1.1 应优先使用 Cache-Control: max-age

2. Cache-Control: max-age

app.get('/cache-control.js', (req, res, next) => {
  fs.readFile(filePath, (error, content) => {
    if (error) return next(error)

    res.set('Cache-Control', 'public, max-age=120')
    return res.type('js').send(content)
  })
})

Cache-Control 强缓存示意

max-age=120 表示响应从生成起在 120 秒内通常可直接复用。响应中的 max-age 与请求中的 Cache-Control: max-age=0 含义不同:前者控制缓存响应的新鲜度,后者是客户端对可接受响应年龄的要求。

常见指令:

# 内容指纹静态资源
Cache-Control: public, max-age=31536000, immutable

# HTML 入口或经常更新的 API
Cache-Control: no-cache

# 个性化数据,只允许浏览器缓存但每次验证
Cache-Control: private, no-cache

# 不希望任何缓存保存
Cache-Control: no-store

no-cache 不是“禁止缓存”,而是允许存储、复用前必须验证;no-store 才是要求不要存储响应。public 也不表示“所有内容默认必须 public”,个性化响应应考虑 private

五、协商缓存实验

强缓存变陈旧后,浏览器可以用验证器询问服务器。如果资源未变化,服务器返回 304 Not Modified,通常不带响应体,浏览器继续使用本地响应。

协商缓存总流程

1. Last-Modified / If-Modified-Since

app.get('/last-modified.js', (req, res, next) => {
  fs.stat(filePath, (statError, stat) => {
    if (statError) return next(statError)

    const lastModified = stat.mtime.toUTCString()
    const headers = {
      'Cache-Control': 'no-cache',
      'Last-Modified': lastModified
    }

    if (req.headers['if-modified-since'] === lastModified) {
      return res.status(304).set(headers).end()
    }

    fs.readFile(filePath, (readError, content) => {
      if (readError) return next(readError)
      return res.status(200).set(headers).type('js').send(content)
    })
  })
})

Last-Modified 条件请求示意

它的缺点包括:HTTP 日期通常只能精确到秒;文件可能在短时间内多次变化;文件可能被修改但内容又恢复原样。对于动态服务和分布式服务器,文件修改时间还需要保持一致。

2. ETag / If-None-Match

ETag 是服务器生成的实体标签,可以是内容哈希、版本号或其他稳定标识。示例使用 SHA-256 生成强 ETag:

const crypto = require('node:crypto')

app.get('/etag.js', (req, res, next) => {
  fs.readFile(filePath, (error, content) => {
    if (error) return next(error)

    const etag = `"${crypto.createHash('sha256').update(content).digest('hex')}"`
    const headers = {
      'Cache-Control': 'no-cache',
      ETag: etag
    }

    // 教学版只比较单个 ETag;生产环境应支持多值和 W/ 弱验证器。
    if (req.headers['if-none-match'] === etag) {
      return res.status(304).set(headers).end()
    }

    return res.status(200).set(headers).type('js').send(content)
  })
})

ETag 条件请求示意

标准请求头的拼写是 If-None-Match,不是原文中的 If-no-matchETagLast-Modified 可以同时返回;如果条件请求同时包含 If-None-MatchIf-Modified-Since,支持 ETag 的服务器应优先处理 ETag。

六、Pragma:保留它的历史位置

Pragma 历史兼容字段示意

Pragma: no-cache 是 HTTP/1.0 时代的兼容字段,历史上常用于要求缓存验证。现代 HTTP 应使用 Cache-Control 表达缓存策略:

Cache-Control: no-cache

浏览器强制刷新时可能同时发送:

Pragma: no-cache
Cache-Control: no-cache

这不意味着存在一个适用于所有浏览器的固定“Pragma > Cache-Control > Expires > ETag > Last-Modified”优先级表。响应缓存新鲜度、请求缓存意图和条件验证是不同层面的规则,必须结合具体请求和响应分析。

七、如何观察实验结果?

1. 浏览器 Network 面板

打开 DevTools 的 Network 面板后观察:

  • Size 是否显示 memory cache、disk cache 或实际传输;
  • 请求是否带 If-None-MatchIf-Modified-Since
  • 响应是 200304 还是被 Service Worker 返回;
  • Cache-ControlExpiresETagLast-ModifiedVary 的具体值;
  • 是否因重定向、认证、Cookie 或请求头变化导致缓存不能共享。

DevTools 中的 “Disable cache” 只在开发者工具打开时影响当前页面调试,不是服务器配置,也不代表真实用户一定禁用缓存。

2. 使用 curl 验证

# 查看首次响应头
curl -i http://localhost:8080/etag.js

# 从上一次响应复制 ETag 后验证
curl -i \
  -H 'If-None-Match: "复制的etag"' \
  http://localhost:8080/etag.js

3. 使用 Fetch 的缓存模式

// 默认由浏览器根据 HTTP 缓存规则决定
fetch('/api/data')

// 允许使用缓存,但复用前向源站验证
fetch('/api/data', { cache: 'no-cache' })

// 要求从网络重新获取(仍受浏览器和网络策略影响)
fetch('/api/data', { cache: 'reload' })

Fetch 的 cache 选项不是关闭所有浏览器、Service Worker、CDN 和服务器缓存的万能开关。

八、实际项目的缓存策略

资源推荐策略
文件名带内容哈希的 JS/CSS/字体public, max-age=31536000, immutable
HTML 入口no-cache,需要及时验证新版本
个性化 APIprivate, no-cache,或按隐私要求使用 no-store
密码、支付和一次性响应通常 no-store
图片根据是否指纹化和更新频率设置 max-age
CDN 公共资源public/s-maxage,并正确设置 Vary

还应注意:

  • Vary: Accept-Encoding 区分 gzip、br 等压缩变体;
  • 根据 Origin 动态返回 CORS 头时,通常需要 Vary: Origin
  • 不要用 Vary: User-Agent 产生大量低命中率变体,能特性检测就不要按 UA 分支;
  • 不要给未版本化的 HTML、JS 文件设置一年 immutable;
  • Service Worker 的 Cache API 是独立的可编程缓存层,需要版本和清理策略;
  • 后退前进可能命中 BFCache,no-cache 也不保证历史恢复一定重新请求。

九、原文中需要修正的字段和结论

  • Expire 应为 Expires
  • If-Modified-Sice 应为 If-Modified-Since
  • If-no-match 应为 If-None-Match
  • Etag 在文档中通常写作 ETag,大小写不影响字段语义,但建议使用标准写法;
  • 没有缓存头不代表浏览器一定每次从服务器重新取资源,可能发生启发式缓存;
  • Expires 没有被“完全废弃”,只是 Cache-Control: max-age 更现代、更可靠;
  • no-cache 不是禁止缓存,no-store 也不会删除已经存在的旧响应;
  • 304 是条件验证成功的无正文响应,不是把完整文件再次传给浏览器;
  • 不存在可机械套用到所有场景的缓存头优先级链。

总结

  1. 先区分浏览器私有缓存、CDN 共享缓存、Service Worker Cache API 和 BFCache;
  2. Cache-Control: max-age 控制新鲜度,Expires 是兼容性的绝对时间;
  3. no-cache 要求复用前验证,no-store 要求不存储;
  4. Last-Modified 精度有限,ETag 可以更准确地表示资源版本;
  5. If-None-MatchIf-Modified-Since 是请求条件,匹配时服务器返回 304
  6. 内容指纹文件可以长缓存,HTML 入口通常需要 no-cache
  7. 用 Network 面板和实际响应头验证,不要只背固定的缓存优先级。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS