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

显示模式

登录
ARCHIVE DOCUMENTBR

关于浏览器缓存你知道多少?

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/15-关于浏览器缓存你知道多少
本文目录12 个章节
  1. 一、先区分“浏览器存储”和“HTTP 缓存”
  2. 二、HTTP 缓存的两种基本模型
  3. 三、强缓存:新鲜响应直接复用
  4. 四、协商缓存:验证资源有没有变化
  5. 五、一个可运行的 Express 缓存示例
  6. 六、Vary:缓存还要看哪些请求头?
  7. 七、刷新、强制刷新和 BFCache
  8. 八、Service Worker 和 Cache API
  9. 九、常见缓存策略
  10. 十、把原文中的字段错误统一修正
  11. 总结
  12. 参考资料

关于浏览器缓存你知道多少?

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

原文主要介绍 HTTP 强缓存、协商缓存、ExpiresCache-ControlLast-ModifiedETag。这些概念仍然是前端面试和性能优化的基础,本文保留原来的学习路径,同时补充私有缓存/共享缓存、Vary、内容指纹、Service Worker、BFCache 和现代缓存策略。

一、先区分“浏览器存储”和“HTTP 缓存”

HTML5 的 Web Storage(localStoragesessionStorage)是 JavaScript 可读写的字符串键值存储;HTTP 缓存保存请求和响应,主要由浏览器、代理、CDN 或 Service Worker 管理。它们都能减少重复工作,但 API、生命周期和安全边界不同。

旧的 HTML Application Cache(manifest/.appcache)已经被主流浏览器移除,不要为新项目使用。离线应用应使用 Service Worker 和 Cache API,并自行设计版本、更新和失效策略。

HTTP 缓存的目标是:在不牺牲正确性和隐私的前提下,复用仍然新鲜的响应,或只向服务器询问“资源是否变化”,避免重复传输完整响应。

二、HTTP 缓存的两种基本模型

按照 HTTP 缓存标准,可以先区分:

  • 私有缓存(private cache):通常是当前用户浏览器的缓存,可以保存个性化响应;
  • 共享缓存(shared cache):位于客户端和源站之间,例如代理、CDN 和反向代理,可能被多个用户复用。

“强缓存”和“协商缓存”是前端常用的学习模型:

  1. 响应仍然新鲜时,缓存可以直接复用,不访问源站;
  2. 响应变为陈旧后,缓存可以携带验证器向服务器重新验证;
  3. 未变化时服务器返回 304 Not Modified,变化时返回新的 200 响应。

真实浏览器还会考虑请求方法、请求头、Vary、凭证、缓存模式、Service Worker、重定向、历史恢复和缓存分区,因此不能把它理解成永远固定的两步判断。

三、强缓存:新鲜响应直接复用

强缓存的典型流程

响应的 Cache-ControlExpires 等字段决定新鲜度。命中时,Network 面板可能显示 memory cachedisk cache 或类似的缓存命中信息;这不等于每个浏览器都会展示完全相同的文字。

1. Expires

Expires 使用绝对时间:

Expires: Wed, 27 Oct 2027 07:55:30 GMT

客户端将当前时间与该时间比较。它是 HTTP/1.0 时代的字段,容易受到时钟偏差和解析兼容性影响。现代服务应优先设置 Cache-Control: max-age=...,但为了兼容旧客户端,仍可能同时返回 Expires

如果同时存在 Cache-Control: max-ageExpires,现代 HTTP 缓存规则优先采用 max-age。HTTP 日期使用 GMT,不应使用本地时区字符串。

2. Cache-Control

Cache-Control: public, max-age=600

响应中的 max-age=600 表示响应从生成起在 600 秒内通常保持 fresh。共享缓存还要考虑 Ages-maxage

常用响应指令:

指令含义
max-age=秒数响应在指定时间内可视为新鲜
s-maxage=秒数只控制共享缓存的新鲜时间,通常覆盖共享缓存的 max-age
public允许共享缓存保存;不是所有响应都必须显式写它
private只能存入私有缓存,适合个性化响应
no-cache可以存储,但复用前必须向源站验证;不是“禁止缓存”
no-store要求缓存不要存储该响应;不会自动删除已经存在的旧缓存
must-revalidate变陈旧后必须验证,不能在与源站断开时随意复用
immutable在 fresh 期间声明内容不会改变,适合内容指纹资源
stale-while-revalidate在指定窗口内允许复用陈旧响应,同时后台验证

publicprivateno-cacheno-store 不能简单当成一条优先级链,它们控制的是不同维度。个性化内容应至少考虑:

Cache-Control: private, no-cache

敏感响应如果完全不应被存储,可以使用:

Cache-Control: no-store

但不要为了“保险”给所有响应都设置 no-store,否则会失去 HTTP 缓存、离线能力和部分性能收益。no-store 也不会清除已经存储的旧响应。

3. 内容指纹和长缓存

不会改变 URL 的静态资源不适合永久缓存。现代构建工具通常为文件名加入内容哈希:

<script src="/assets/app.8d31c2.js"></script>
<link rel="stylesheet" href="/assets/style.4fa91e.css">

当内容改变时生成新 URL,旧 URL 仍然可以安全长缓存:

Cache-Control: public, max-age=31536000, immutable

HTML 入口通常需要更快感知新版本,可以使用:

Cache-Control: no-cache

这允许浏览器保存 HTML,但每次复用前向服务器验证;服务器没有变化时仍可返回 304

四、协商缓存:验证资源有没有变化

协商缓存的典型流程

当响应需要验证时,浏览器会把之前响应中的验证器放入请求头。服务器确认资源没有变化时返回 304 Not Modified,通常不带响应体;浏览器继续使用本地响应,并根据新的响应头更新缓存元数据。

1. Last-Modified / If-Modified-Since

首次响应:

HTTP/1.1 200 OK
Last-Modified: Fri, 27 Oct 2027 07:55:30 GMT
Cache-Control: no-cache

...resource body...

验证请求:

GET /assets/app.js HTTP/1.1
If-Modified-Since: Fri, 27 Oct 2027 07:55:30 GMT

资源没有变化时:

HTTP/1.1 304 Not Modified
Last-Modified: Fri, 27 Oct 2027 07:55:30 GMT
Cache-Control: no-cache

这个方案容易实现,但 HTTP 日期精度通常为秒:同一秒内多次修改、文件时间同步问题或内容改回原样时,单靠修改时间可能不够准确。

2. ETag / If-None-Match

首次响应:

ETag: "app-8d31c2"
Cache-Control: no-cache

验证请求:

If-None-Match: "app-8d31c2"

服务器根据当前资源的 ETag 比较:

  • GET/HEAD 的 ETag 匹配:返回 304
  • 不匹配:返回新的 200 和新的响应体;
  • 修改类请求还可以使用 If-Match 防止覆盖其他人的更新,失败时返回 412 Precondition Failed

ETag 可以是内容哈希、版本号、文件元数据或其他服务器生成的标识,不强制要求使用 MD5。ETag 应包含双引号,弱验证器可能以 W/ 开头。

如果请求同时带有 If-None-MatchIf-Modified-Since,支持 ETag 的服务器应优先处理 If-None-Match。实际项目通常同时返回 ETagLast-Modified,以兼容不同客户端并支持其他 HTTP 工具。

五、一个可运行的 Express 缓存示例

1. 推荐:静态资源使用 express.static

Express 静态中间件本身可以提供 ETag 和 Last-Modified。Node.js 18+ 的 Express 5 示例:

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

const app = express()
const port = 8080

// 内容指纹资源:文件内容变化时文件名也变化,可以长时间缓存。
app.use('/assets', express.static(path.join(__dirname, 'static'), {
  etag: true,
  lastModified: true,
  maxAge: '1y',
  immutable: true
}))

// HTML 入口:允许缓存,但访问时验证新版本。
app.get('/', (req, res) => {
  res.set('Cache-Control', 'no-cache')
  res.type('html').send(`<!doctype html>
<html lang="zh-CN">
  <head><meta charset="utf-8"><title>HTTP Cache Demo</title></head>
  <body><script src="/assets/demo.js"></script></body>
</html>`)
})

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

不要给会经常变化的 demo.js 直接加一年缓存,除非它的 URL 会随内容指纹变化。

2. 手动演示验证器

如果想观察请求头和 304,可以写一个简化版本。示例仅用于学习,真实服务应处理文件不存在、并发读取、压缩变体和多值 ETag:

const crypto = require('node:crypto')
const fs = require('node:fs')

app.get('/demo.js', (req, res, next) => {
  const filePath = path.join(__dirname, 'static', 'demo.js')

  fs.stat(filePath, (statError, stat) => {
    if (statError) return next(statError)

    fs.readFile(filePath, (readError, body) => {
      if (readError) return next(readError)

      const lastModified = stat.mtime.toUTCString()
      const etag = `"${crypto.createHash('sha256').update(body).digest('hex')}"`
      const headers = {
        'Cache-Control': 'no-cache',
        'Content-Type': 'text/javascript; charset=utf-8',
        ETag: etag,
        'Last-Modified': lastModified
      }

      // 教学版只处理一个 ETag;生产环境应解析逗号分隔的值和 W/ 前缀。
      if (req.headers['if-none-match'] === etag) {
        return res.status(304).set(headers).end()
      }

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

      return res.status(200).set(headers).send(body)
    })
  })
})

304 不是“服务器再次发送了缓存内容”,而是服务器确认浏览器已有的表示仍可使用。304 响应不应包含新的响应体。

六、Vary:缓存还要看哪些请求头?

同一个 URL 的响应可能因 Accept-EncodingAccept-LanguageOrigin 等请求头不同而不同。此时需要明确告诉缓存区分方式:

Vary: Accept-Encoding, Origin

例如服务端根据请求的 Origin 动态返回 Access-Control-Allow-Origin,就应考虑 Vary: Origin,避免共享缓存把一个来源的 CORS 响应错误地复用给另一个来源。

不要轻易使用 Vary: User-Agent,它可能产生大量缓存变体,显著降低命中率。能用特性检测解决的问题,不要按 User-Agent 做大量分支。

七、刷新、强制刷新和 BFCache

开发者工具中的“刷新”“强制刷新”、地址栏重新导航和后退前进不是同一个缓存动作:

  • 普通刷新可能发送 Cache-Control: max-age=0 并携带条件请求头;
  • 强制刷新通常会发送 Cache-Control: no-cache,可能同时带 Pragma: no-cache
  • Pragma 是 HTTP/1.0 时代的兼容字段,不能简单说它永远比 Cache-Control 优先;
  • 后退前进可能命中 BFCache,直接恢复页面快照,不一定重新验证 HTTP 响应;
  • JavaScript 中 fetch(url, { cache: 'no-cache' }) 表示复用前验证,cache: 'reload' 才更接近要求重新从网络获取。

如果要测试缓存,打开 Network 面板并注意“Disable cache”只在 DevTools 打开期间生效,不能把开发工具行为当成线上用户行为。

八、Service Worker 和 Cache API

Service Worker 可以拦截受其 scope 控制的 Fetch,并使用 Cache API 建立自定义缓存:

self.addEventListener('fetch', event => {
  const request = event.request

  if (request.destination !== 'script') return

  event.respondWith(
    caches.match(request).then(cached => {
      const network = fetch(request).then(response => {
        const copy = response.clone()
        caches.open('assets-v1').then(cache => cache.put(request, copy))
        return response
      })

      return cached || network
    })
  )
})

Cache API 不是 HTTP 缓存的简单别名:

  • Service Worker 可以自定义缓存优先、网络优先、过期回源等策略;
  • Response body 是流,缓存前通常需要 response.clone()
  • 需要设计缓存版本和旧缓存清理;
  • 不要缓存包含用户敏感数据的响应;
  • Service Worker 的缓存策略可能覆盖开发者对 HTTP 缓存的直觉判断。

九、常见缓存策略

资源推荐示例原因
内容指纹 JS/CSS/字体public, max-age=31536000, immutableURL 变化代表内容变化
图片按是否指纹化设置较长 max-age兼顾带宽和更新速度
HTML 入口no-cache可以验证并及时获取新资源引用
个性化 APIprivate, no-cache 或按需求 no-store避免共享缓存泄露
密码、支付、一次性响应通常 no-store不希望客户端或中间缓存保存
CDN 公共响应public + s-maxage + 合适 Vary将共享缓存策略说清楚

不要只看到“命中缓存”就认为一定更快:缓存命中依然可能有存储读取、Service Worker 执行、解压、解析和主线程成本;响应也必须与当前请求上下文匹配。

十、把原文中的字段错误统一修正

  • Expire 应写作响应头 Expires
  • If-Modified-Sice 应写作 If-Modified-Since
  • If-no-match 应写作 If-None-Match
  • Last-ModifiedETag 是响应验证器,If-* 字段是客户端后续请求携带的条件;
  • no-cache 不是“不缓存”,no-store 才是“不存储”;
  • ETag 不一定是文件 MD5,服务器可以使用版本号或其他标识;
  • 不存在适用于所有浏览器、所有场景的“Pragma > Cache-Control > Expires > ETag > Last-Modified”固定优先级表;
  • 服务器返回 304 时通常不带实体正文,浏览器复用已有响应体。

总结

  1. HTTP 缓存分为私有缓存和共享缓存,强缓存/协商缓存是理解缓存行为的常用模型;
  2. Cache-Control: max-age 是现代新鲜度控制核心,Expires 主要用于兼容;
  3. no-cache 要求复用前验证,no-store 要求不存储,二者不能混为一谈;
  4. ETagLast-Modified 都有用,If-None-Match 在验证时优先级更高;
  5. 内容指纹 + 长缓存适合静态资源,HTML 入口通常使用 no-cache
  6. Vary、Cookie、CORS、Service Worker、CDN 和 BFCache 都会影响最终结果;
  7. 缓存配置必须结合资源是否个性化、是否可版本化和实际 Network 记录验证。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS