news 2026/8/29 12:33:38

【浏览器】强缓存和协商缓存(HTTP 缓存)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【浏览器】强缓存和协商缓存(HTTP 缓存)

部分内容来源:渡一教育。

为什么有缓存?

来自服务器的缓存指令

当客户端发出一个get请求到服务器,服务器希望客户端将相应资源缓存起来,服务器在响应头中加入了以下内容:

Cache-Control:max-age=3600 ETag:W/"121-171ca289ebf" Date:Thu,30 Apr 2020 12:39:56 GMT Last-Modified:Thu, 30Apr2o20 08:16:31GMT

这个响应头表达了下面的信息:

  • Cache-Control:max-age=3600, 缓存时间是3600秒(1小时)
  • ETag:W/“121-171ca289ebf”, 这个资源的编号(可以认为是哈希/唯一id)是W/“121-171ca289ebf”
  • Date:Thu,30Apr202012:39:56GMT, 我给你响应这个资源的服务器时间是格林威治时间2020-04-3012:39:56
  • Last-Modified:Thu,30Apr2020g8:16:31GMT, 这个资源的上一次修改时间是格林威治时间2020-04-3088:16:31

如果客户端是其他应用程序,可能并不会理会服务器的愿望,也就是说,可能根本不会缓存任何东西。

如果刚好是一个浏览器,那么就会执行缓存:

  • 浏览器把这次请求得到的响应体缓存到本地文件中
  • 浏览器标记这次请求的请求方法和请求路径
  • 浏览器标记这次缓存的时间是3600秒
  • 浏览器记录服务器的响应时间是格林威治时间2020-04-3012:39:56
  • 浏览器记录服务器给予的资源编号W/“121-171ca289ebf”
  • 浏览器记录资源的上一次修改时间是格林威治时间2020-04-3088:16:31

来自客户端的缓存协议

当客户端准备再次请求GET /index.js时,它突然想起了一件事:我需要的东西在不在缓存里呢?

此时,客户端会到缓存中去寻找是否有缓存的资源,寻找的过程如下:

  1. 缓存中是否有匹配的请求方法和路径?
  2. 如果有,该缓存资源是否有效呢?

以上两个验证会导致浏览器产生不同的行为:

缓存无效(协商缓存)

如果缓存有效则直接使用缓存结果。当浏览器发现缓存已经过期,它并不会简单的把缓存删除,而是抱着一丝希望,想问问服务器,我这个缓存还能继续使用吗?

于是,浏览器向服务器发出了一个带缓存的请求,又称之为协商缓存。

所谓带缓存的请求,无非就是加入了以下的请求头:

If-Modified-Since: Thu,30 Apr 2020 08:16:31 GMT If-None-Match: W/"121-171ca289ebf"

它们表达了下面的信息:

  • If-Modified-Since:Thu,30Apr202088:16:31GMT 这个资源
    的上一次修改时间是格林威治时间2020-04-3088:16:31,请问这个资源在这个时间之后有发生变动吗?
  • If-None-Match:W/“121-171ca289ebf” 这个资源的编号是W/"121-171ca289ebf, 请问这个资源的编号发生变动了吗?

其实,这两个问题可以合并为一个问题:就是问服务器资源到底变化了没有?!

之所以要发两个信息,是为了兼容不同的服务器,因为有些服务器只认If-Modified-Since,有些服务器只认If-None-Match(优先级更高),有些服务器两个都认。

服务器可能会产生两个情况:

  • 缓存已经失效
  • 缓存仍然有效

如果是第一种情况一一缓存已经失效,那么非常简单,服务端再次给予一个正常的响应(响应码 200 带响应体),同时可以附带上新的缓存指令,这就回到了上一节一一来自服务器的缓存指令。

这样一来,客户端就会重新缓存新的内容。

但如果服务器觉得缓存仍然有效,它可以通过一种极其简单的方式告诉客户端:

  • 响应码为 304 Not Modified (原本响应内容未修改)
  • 无响应体
  • 响应头带上新的缓存指令,见上一节一一来自服务器的缓存指令

这样一来,就相当于告诉客户端:「你的缓存资源仍然可用,我给你一个新的缓存时间,你那边更新一下就可以了」

于是,客户端就继续使用缓存了。

这样一来,可以最大程度的减少网络传输,因为如果资源还有效,服务器就不会传输消息体。

强缓存和协商缓存

http 缓存分为强缓存和协商缓存。他们用于优化网站性能并减少服务器负载。这两种缓存都通过HTTP 响应头来控制,它们分别基于不同的缓存验证方式,可以根据资源的特性和需求来选择合适的缓存策略。

客户端

<html><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><title>Document</title></head><body><buttonid="btn">send</button><script>constbtn=document.querySelector('#btn')btn.addEventListener('click',()=>{fetch('http://localhost:3000/api3')})</script></body></html>

1.强缓存(Strong Cache)

  • 客户端在请求资源时,会检查缓存的相关响应头(如使用Cache-ControlExpires来控制缓存在本地的有效期)进行缓存
  • 后续的请求,如果资源的缓存尚未过期(存在 memory cache / disk cache),客户端将直接从本地缓存中获取资源,而不会发送请求到服务器。
  • 常用的强缓存响应头包括:
    • Cache-Control: max-age=<seconds>指定资源的缓存时间,单位为秒HTTP1.1 提出,优先级更高,请求头和响应头都支持这个属性,通过它提供的不同的值来定义缓存策略。此外,Cache-Control还支持其他的属性字段,如no-cache 、no-store、public、private 、must-revalidate等。
    • Expires: <date>指定资源的过期时间,是一个日期字符串,表示绝对时间,由服务器返回。它是一个HTTP 1.0的头部字段。受限于本地时间,修改了本地时间,可能会导致缓存失效。如果在Expires之内,则浏览器会直接读取缓存,不再请求服务器。
  • 从浏览器读取缓存分为内存缓存(memory cache,浏览器内存,关闭页面后会被清除,通常用于在同一个页面会话中的快速重复请求)和硬盘缓存(disk cache,计算机硬盘,空间大,读取效率低,持久化存储资源,直到缓存策略决定缓存到期或被手动清除),而这两种缓存策略由浏览器自身分配。(此外, 还有 Service Worker 缓存:应用程序控制的缓存层,通过 Service Worker 脚本精确控制缓存和资源请求逻辑)

浏览器请求顺序:

1. Memory Cache 2. Service Worker Cache 3. HTTP Disk Cache 4. 网络请求
  • 状态码为 200

network:

结果表示
from disk cache强缓存(磁盘缓存命中)
from memory cache强缓存(内存缓存命中)
from network走网络请求,但不一定是协商缓存,还需看 Header
importexpressfrom'express'importcorsfrom'cors'importfsfrom'node:fs'importcryptofrom'node:crypto'constapp=express()app.use(cors())//静态资源缓存 html, css, js, png// app.use(express.static('./static', {// maxAge: 100 * 60 * 5,// lastModified: true// }))//动态资源缓存//Expiresapp.get('/api',(req,res)=>{// 到了过期时间之后将不再进行缓存res.setHeader('Expires',newDate('2024-4-6 8:57:00').toUTCString())res.send('Expires1111')})// Cache-Control// public 任何服服务器都可以缓存包括代理服务器 cdn// private 只能浏览器缓存 不包括代理服务器// max-age 缓存的时间app.get('/api2',(req,res)=>{res.setHeader('Cache-Control','public, max-age=10')res.send('Cache-Control1111')})app.listen(3000,()=>{console.log('3000端口已启用')})

2.协商缓存(Conditional Cache)

当浏览器对某个资源的请求没有命中强缓存(当强缓存(如 max-age)过期或响应头设置 no-cache时),就会发一个请求(包含If-Modified-SinceIf-None-Match的请求头)到服务器,验证协商缓存是否命中(判断资源是否更新)。

协商缓存无过期时间。

如果协商缓存命中(资源仍可以继续使用),请求响应返回的HTTP状态为304 (Not Modified),该请求不携带实体数据浏览器复用本地缓存),若未命中,则返回200并携带资源实体数据

协商缓存是利用的是Last-Modified,If-Modified-Since和ETag、If-None-Match这两对Header来管理的。

  • 常用的协商缓存响应头包括:

    • Last-Modified指定资源的最后修改时间HTTP1.0提出。浏览器会在请求头加上If-Modified-Since即上次响应的Last-Modified的值,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,但是如果在本地打开缓存文件(或者重新编译),就会造成Last-Modified被修改,因此导致误判,而且他在 1s 内多次修改也无法检测得到,所以在HTTP 1.1出现了ETag。
    • ETag指定资源的实体标签(服务器生成资源的唯一标识符 / 哈希)HTTP1.1提出,只有资源变化才会被修改(跟最后修改时间没有关系)。If-None-Match 的请求头字段会将上次返回的 ETag 发送给服务器,询问该资源的 ETag 是否有更新,有变动就会发送新的资源回来。ETag 的优先级比 Last-Modified 更高。

    强 ETag(无前缀):内容任何变化都会更新标识。
    弱 ETag(W/前缀):仅当内容语义变化时更新,如 ETag: W/“0815”。

  • 强缓存和协商缓存同时出现,默认强缓存的优先级更高。

ETag 判断条件场景:文件由版本1变到版本2,再变回版本1,etag是否还原:“ETag 是否还原不由 HTTP 协议保证,取决于服务器的 ETag 生成策略。如果是基于内容 hash,文件回退后 ETag 会一致;如果基于时间戳、版本号或构建号,即使内容相同,ETag 也可能不同。”

  • Last-Modified 和 ETag 的区别:Last-Modified精度只能到秒,但是性能更好。ETag更精细,但是性能不如前者,因为文件变化都要重新计算hash值。
importexpressfrom'express'importcorsfrom'cors'importfsfrom'node:fs'importcryptofrom'node:crypto'constapp=express()app.use(cors())//获取文件的最后修改时间constgetFileModifyTime=()=>{returnfs.statSync('./index.js').mtime.toISOString()}app.get('/api3',(req,res)=>{// no-cache 使用协商缓存而不是强缓存// no-store 不走任何缓存res.setHeader('Cache-Control','no-cache')constmodifyTime=getFileModifyTime();// 浏览器根据 Last-Modified 的值自动设置 if-modified-since 的值(与上一次 Last-Modified 的值相同)constifModifiedSince=req.headers['if-modified-since']// 如果文件不修改则不用重新缓存if(ifModifiedSince===modifyTime){console.log('缓存了')res.statusCode=304res.end()return}console.log('没有缓存')res.setHeader('Last-Modified',modifyTime)res.send('Last-Modified111')})app.listen(3000,()=>{console.log('3000端口已启用')})
importexpressfrom'express'importcorsfrom'cors'importfsfrom'node:fs'importcryptofrom'node:crypto'constapp=express()app.use(cors())//获取文件内容哈希constgetFileHash=()=>{returncrypto.createHash('sha256').update(fs.readFileSync('index.js')).digest('hex')}app.get('/api3',(req,res)=>{// no-cache 使用协商缓存而不是强缓存// no-store 不走任何缓存res.setHeader('Cache-Control','no-cache')constfileHash=getFileHash();constifNoneMatch=req.headers['if-none-match']// 如果文件不修改则不用重新缓存if(ifNoneMatch===fileHash){console.log('缓存了')res.statusCode=304res.end()return}console.log('没有缓存')res.setHeader('ETag',fileHash)res.send('Etag111')})app.listen(3000,()=>{console.log('3000端口已启用')})

强缓存和协商缓存可以结合使用,以提高缓存效果。通常情况下,可以首先使用强缓存来尽可能减少对服务器的请求,如果资源已经过期或者需要重新验证,则使用协商缓存来验证缓存的有效性。

这样可以在保证性能的同时,确保客户端始终能够获取到最新的资源。

知识点:同一个接口、Web 可以走缓存但移动端不行

视频:

Cache-Control: public, max-age=31536000 ETag: "abc" Accept-Ranges: bytes

浏览器行为:

  • 第一次请求:200 OK + 视频资源
  • 后续:
    • 读取本地 cache
    • 或用 If-None-Match 请求确认 → 304 Not Modified
      原生 App / React Native / Flutter 行为:
  • 使用 Range 分段请求(按字节流式加载)
  • 每次进入页面重新从服务器拉
  • 默认不开磁盘缓存(即使服务端设置了缓存)

Service Worker 缓存

PWA (渐进式的 web app),旨在将 web 像是原生桌面 app 那样使用,可安装可卸载。其中,service worker 是 pwa 的核心。

Service Worker 是运行在浏览器后台的独立线程,可拦截网络请求并实现精细化缓存管理,显著提升离线体验和加载性能。

基本使用

  • ​离线缓存​
    预缓存关键资源(HTML/CSS/JS),确保无网络时应用仍可运行(可以在控制台 Application -> Cache Storage 中看到 workbox 缓存了该应用必要的文件):

    // service-worker.js const CACHE_NAME = 'v1'; const urlsToCache = ['/', '/styles.css', '/app.js']; self.addEventListener('install', (event) => { event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))); }。

  • ​动态缓存策略​
    根据资源类型选择策略:

    • ​Cache First​​:静态资源(如图片)优先从缓存读取。
    • ​Network First​​:动态数据(如API)优先网络请求,失败时回退缓存。
    • ​Stale While Revalidate​​:先返回缓存,后台更新缓存(适合频繁更新资源)。

​缓存更新与冲突解决​

  • ​版本控制​
    更新缓存名称(如my-cache-v2),激活时清理旧缓存:

    self.addEventListener('activate', (event) => { caches.keys().then(names => names.map(name => { if (name !== CACHE_NAME) caches.delete(name); })) }。
  • ​数据一致性保障​

    • 对关键数据采用 ​​Network First 策略​​,确保获取最新结果。
    • 通过 ​​Background Sync API​​ 在网络恢复后同步数据。

​资源优先级管理​

按需加载非关键资源,加速首屏渲染:

self.addEventListener('fetch', (event) => { if (event.request.url.includes('/non-critical/')) { // 延迟加载非关键资源 event.respondWith(caches.match(event.request)); } else { // 关键资源直接网络请求 event.respondWith(fetch(event.request)); } }。

与 WebSocket 协同​

  • ​实时更新缓存​​:
    WebSocket 接收更新通知 → Service Worker 后台刷新缓存:

    // WebSocket 监听更新 socket.onmessage = (event) => { if (event.data.type === 'DATA_UPDATE') { caches.open('my-cache').then(cache => cache.put('/api/data', new Response(event.data.payload))); } }。
  • ​离线消息队列​​:
    Service Worker 暂存离线期间的 WebSocket 消息,网络恢复后重发。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/29 12:31:18

STM32G031J6M6修改NRST_MODE失败问题实战解析

最近在调一块基于 STM32G031J6M6 的小板子&#xff0c;想把复位引脚 NRST 从默认的双向复位模式改成普通复位输入模式&#xff0c;结果被 STM32CubeProgrammer 反复提示 Unable to change NRST_MODE&#xff0c;前前后后折腾了一个下午。这个报错在 STM32G0 系列上其实不算冷门…

作者头像 李华
网站建设 2026/8/29 12:27:39

进程间的通信

一、概念 进程间通信&#xff08;Inter-Process Communication&#xff0c;IPC&#xff09;是指在不同进程之间进行信息交换和同步的机制。 二、实现方式 1、管道 &#xff08;1&#xff09;无名管道 原理&#xff1a;它是一种半双工的通信方式&#xff0c;数据只能单向流动&…

作者头像 李华
网站建设 2026/8/29 12:26:59

REDRIVER2:用C语言重写《Driver 2》的开源逆向项目

还记得 PlayStation 时代那些用光盘才能玩到的经典作品吗&#xff1f;《Driver 2》就是其中一款让无数老玩家念念不忘的赛车动作游戏——它允许你中途下车、劫车、在城市里自由穿梭&#xff0c;这种“开放式驾驶”设计在 2000 年可以说相当超前。但 20 多年过去&#xff0c;你想…

作者头像 李华
网站建设 2026/8/29 12:26:45

10行代码接入世界模型:QQWorld双查询机制解析与实战

看到世界模型&#xff08;World Model&#xff09;相关的工作越来越多&#xff0c;很多同学在复现时都被环境搭建、状态空间设计、模型训练开销劝退了。近期西安交通大学团队开源的 QQWorld 引起了不少关注&#xff0c;它的宣传点非常直接&#xff1a;仅需 10 行代码&#xff0…

作者头像 李华
网站建设 2026/8/29 12:19:28

美赛数学建模实战指南:从组队到论文的96小时极限挑战

1. 从旁观到入局&#xff1a;我眼中的美赛到底是什么&#xff1f; 第一次听说“美赛”&#xff08;美国大学生数学建模竞赛&#xff0c;MCM/ICM&#xff09;的时候&#xff0c;我还在大二&#xff0c;看着学长学姐们熬了四天三夜后蓬头垢面的样子&#xff0c;心里既好奇又有点发…

作者头像 李华
网站建设 2026/8/29 12:19:21

分治算法精讲:从归并排序到快速幂,掌握高效问题解决范式

1. 项目概述&#xff1a;分治策略的核心思想 “分治”这个词听起来挺玄乎&#xff0c;但它的核心思想其实非常朴素&#xff0c;就像我们处理生活中的复杂问题一样。想象一下&#xff0c;你要整理一个杂乱无章、堆满各种物品的大仓库。如果试图一次性把所有东西都归位&#xff0…

作者头像 李华