ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

浏览器缓存机制全解析:从强缓存到协商缓存的实践指南

浏览器缓存机制全解析:从强缓存到协商缓存的实践指南 每次发版之后总有人来问我“我明明清了缓存怎么页面还是旧的”或者说“为什么我手机上是新版本电脑上打开还是老样式”。这类问题背后绕不开的就是资源加载和缓存机制。这篇原理篇就把浏览器从输入URL到拿到资源的完整路径拆开看一遍重点讲清楚缓存机制在设计上到底是怎么工作的以及我们在日常开发里应该怎么利用这些规则而不是被它们坑。这个话题适合前端开发者、性能优化工程师也适合后端同学了解浏览器行为的边界。弄懂这一块很多线上诡异问题都能一眼定位。1. 资源加载的完整链路拆解1.1 从URL输入到资源就绪浏览器到底做了什么很多人觉得资源加载就是“浏览器发个请求服务器返回文件”但实际链路比这个长得多。我习惯把它拆成四段DNS解析把域名解析成IP地址。这一步听起来简单但DNS缓存、DNS预解析、CDN调度都会在这里介入。建立连接TCP三次握手HTTPS还要加一次TLS握手。HTTP/1.1有连接复用HTTP/2有连接多路复用这个差异会影响多个资源的加载速度。发送HTTP请求浏览器根据资源类型生成请求头重点关注Cache-Control、If-None-Match、If-Modified-Since这些缓存相关字段。接收响应并处理服务器返回状态码和响应头浏览器根据响应头决定“是直接用本地缓存还是把新文件存下来还是每次都要问服务器”。其中缓存机制作用在最后两段之间。浏览器收到响应后会根据响应头把资源缓存到本地下一次请求同样资源时浏览器先问“我这个缓存还能不能用”而不用傻乎乎地重新下载。你可以把整个过程理解成去食堂打饭DNS解析相当于查地图找食堂位置TCP连接相当于排队走到窗口HTTP请求相当于递盘子而缓存机制就是“上次多打了一份放桌上这次先看看还能不能吃”。剩菜能吃就直接吃不用再去窗口排队不确定能不能吃就端着盘子问阿姨“这份还行不行”阿姨说行你就端走说不行你再重新打。1.2 为什么加载链路和缓存机制是绑在一起看的我见过不少团队把“资源加载优化”和“缓存配置”分成两件事做结果就是加载优化做了图片压缩、代码分割、CDN加速但缓存策略没跟上导致用户每次打开页面都要重新下载一堆根本没变化的文件前面的优化白费了。反过来也有问题缓存策略配得太强文件名不变资源永远走本地缓存发版之后用户看到的还是旧页面。所以这两个话题必须放在一起理解才完整。资源加载链路决定了缓存机制在哪个环节生效缓存机制反过来决定了加载链路的实际开销。理解了这条链路的全貌你就知道为什么Cache-Control: max-age31536000配contenthash文件名是黄金组合也知道为什么index.html要设成no-cache。2. 浏览器缓存机制的两大核心2.1 强缓存Cache-Control与Expires的取舍强缓存的意思是浏览器在缓存有效期内直接使用本地副本不发任何请求到服务器。控制它的头有两个一个是ExpiresHTTP/1.0产物一个是Cache-ControlHTTP/1.1标准。先看看Expires的写法Expires: Wed, 21 Oct 2026 07:28:00 GMT它的问题是用的是一个绝对时间。服务器时间和用户本地时间不一致的话缓存判断就会出错。用户把系统时间改早了一小时缓存就能多活一小时改晚了缓存提前失效又得重新请求。Cache-Control是现在的主力它用相对时间避免了这个坑Cache-Control: max-age3600这就告诉浏览器这个资源在3600秒内都新鲜直接用本地缓存就行。但Cache-Control不只是max-age它的指令组合才是精髓。举几个我实际配置过的组合Cache-Control: no-cache名字很容易让人误解它并不是禁止缓存而是“每次使用前必须去服务器验证一下”。验证通过返回304就用缓存验证不通过就下载新的。这是HTML文件最常见的配置。Cache-Control: no-store这才是真正的完全不缓存每次都要完整下载。登录态接口、交易请求建议这么配。Cache-Control: max-age31536000, immutable一年内绝对新鲜且不允许更新。搭配带hash的文件名使用强缓存拉满性能最优。我在项目里给静态资源配的一般是最后这种配完基本不再有“文件没更新”的困扰因为文件名变了URL就变了不存在旧缓存覆盖的问题。2.2 协商缓存Last-Modified与ETag是怎么配合的协商缓存的意思就是浏览器发请求时带上自己那份缓存文件的“身份证”服务器看一眼觉得你那份还能用就返回304 Not Modified不返回文件体节省带宽。这个“身份证”有两套体系第一套基于时间Last-Modified服务器返回和If-Modified-Since浏览器下次请求时带上。响应头: Last-Modified: Tue, 15 Nov 2024 12:45:26 GMT 请求头: If-Modified-Since: Tue, 15 Nov 2024 12:45:26 GMT这套方案有个硬伤时间粒度只能到秒。文件在1秒内改了两次第二次修改的时间跟第一次没区别服务器判断“没变”返回304浏览器就拿到了旧内容。另外服务器和服务器之间时间不同步也会导致判断出错。第二套基于内容指纹ETag和If-None-Match。服务器根据文件内容计算出一个哈希值内容变了哈希值就变。响应头: ETag: 33a64df551425df0935f8a6d1a4c4f0a 请求头: If-None-Match: 33a64df551425df0935f8a6d1a4c4f0a这里我要多说一句不同服务器的ETag计算方式不一样Nginx默认用的是文件修改时间加文件大小算的所以ETag用得好不好很依赖服务器配置和框架实现。下面这个表格能帮你快速记住差异对比项强缓存协商缓存请求是否发出不发出直接本地取发出但文件体不重复下载关键头Cache-Control / ExpiresLast-Modified / ETag状态码200 (from cache)304优点零网络开销极快保证内容新鲜节省带宽缺点缓存更新不可控仍有HTTP请求往返延迟高实际场景中强缓存和协商缓存经常叠加使用。比如给图片配置Cache-Control: max-age86400同时让它带ETag。缓存还在有效期时走强缓存过期之后走协商缓存304就继续用内容变了就重新下载。两条链路配合起来才算是一个完整的缓存策略。2.3 内存缓存和磁盘缓存的区别这是很多人忽略的细节。浏览器本地缓存放哪是有讲究的。Chrome的Network面板里你能看到两种标识from memory cache和from disk cache。from memory cache资源被缓存在内存里读取速度极快但进程关闭就没了。通常是当前页面渲染时需要的脚本、样式、图片。from disk cache资源被缓存在磁盘上读取速度相对慢一些但持久化保留浏览器重启之后依然有效。大文件、跨会话的资源一般在这里。浏览器决定放内存还是放磁盘主要依据资源大小和当前页面的内存压力。这属于浏览器内部策略我们开发时无法直接控制但了解它有助于解读Network面板里的现象。比如你刷新页面发现JS走的是from memory cache关掉标签页重新打开变成from disk cache这其实是正常的缓存层级变化不是配置出了问题。内存缓存和磁盘缓存之间的这个差异还有一个实际影响同一个资源在内存里读取是不走网络栈的所以你在Performance面板里看到的资源耗时会有明显差异。你要是没分清这个排查性能问题时就容易误判。3. 缓存策略设计与版本更新最佳实践3.1 静态资源指纹与文件名hash策略讲缓存绕不开一个经典难题如何让用户在缓存有效期内尽可能复用资源同时又在发新版本时第一时间拿到最新代码答案是文件名带上内容哈希。现在Webpack、Vite、Rollup都默认支持这个能力产物长这样app.8f4c3b9a.js vendor.2e7d5c11.js index.c9be0f4a.css文件名里的8f4c3b9a就是内容哈希文件内容一变哈希就变文件名就变URL就变对于浏览器来说这就是一个全新的资源强缓存对它没有任何影响。这套方案的精髓在于分两层看待HTML文件配置Cache-Control: no-cache每次都要回源验证。文件名变化HTML里引用的URL就会变成新的浏览器自然就去请求新文件。静态资源JS/CSS/图片配置Cache-Control: max-age31536000, immutable一年内强缓存。因为文件名变了URL才变文件名不变说明内容真的没变缓存一年完全没问题。实际项目里我见过配反的情况HTML配了max-age3600静态资源配了no-cache。结果发版后一小时内用户看到的都是旧页面静态资源却每次都要回源验证性能损耗巨大。这个配置顺序千万别弄反。有人会问immutable这个指令有什么实际意义它的作用是告诉浏览器“这个文件在当前版本内不会变”所以即使用户手动刷新页面强刷新除外浏览器也不用重新验证。浏览器默认刷新时会对过期但没过期的资源重新请求实际上对于没过期的强缓存资源普通刷新一般会继续用缓存但加不加immutable在最老版本的Chrome和Edge里有行为差异。加上没坏处而且语义明确。3.2 CDN与缓存的组合打法静态资源上了CDN之后缓存链路又多了一层同时也多挂了几个关键头。CDN节点本质上也遵循HTTP缓存语义但它和浏览器缓存的关注点不太一样。这里要搞清楚Cache-Control里一个容易被忽略的指令s-maxage。Cache-Control: max-age3600, s-maxage86400这个配置的意思分两级浏览器本地缓存一小时CDN节点缓存一天。s-maxage优先覆盖max-age但只对共享缓存典型代表就是CDN中间节点生效个人浏览器会忽略它。实际使用中我是这么配静态资源的Cache-Control: max-age31536000, s-maxage31536000, immutable如果你不希望CDN缓存HTML因为HTML需要实时感知发版那就给HTML单独配置Cache-Control: no-cache并且让CDN在回源时完全透传这个头不做覆盖。这里有一个常见的坑很多团队的CDN平台上默认开启了“强制缓存”或“改写源站缓存头”的选项源站明明设了no-cacheCDN平台却强制给你设成max-age600结果就是发版后10分钟内用户拿到的还是旧HTML。排查这类问题时别只盯着源站Nginx配置也要看看CDN控制台里的缓存配置。使用CDN时还有一个“缓存键”的概念很关键。CDN默认的缓存键往往是整个URL但部分云厂商允许自定义缓存键比如去掉URL里的某个参数、忽略请求头里的某个字段。如果你配置不合理会导致CDN命中率异常低下。比如你在URL上挂了一个随机的?timestampxxx参数CDN会认为这是无数个不同资源每一次请求都回源缓存形同虚设。所以我一直强调CDN默认元数据不对业务无所谓但一旦你开始往线上加参数务必同时确认CDN缓存键的配置能兜住。3.3 Service Worker与缓存进阶Service Worker是另一套缓存体系它在浏览器层面拦截请求与HTTP缓存的语义基本不同优先级更高、更灵活。我自己的项目里用过这么几种策略Cache First缓存优先命中缓存直接返回不请求网络。适合图片、图标、稳定的静态资源。Network First网络优先先请求网络失败或超时后退回缓存。适合页面HTML、需要快速感知更新的数据接口。Stale While Revalidate过期时后台更新立即返回缓存内容同时后台发起网络请求更新缓存。适合对时效要求不高但希望更新平滑的资源。Service Worker缓存设计有一个容易踩的坑如果你使用了Cache First策略但缓存名字是固定的比如my-app-assets-v1那发版之后旧缓存还在用户看到的还是旧资源。解决办法是把版本号加进缓存名升级时删除旧缓存const CACHE_NAME my-app-assets-v2; self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) { return cache.addAll([/, /index.css, /index.js]); }) ); }); self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((keys) { return Promise.all(keys.filter((key) key ! CACHE_NAME).map((key) caches.delete(key))); }) ); }); self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request).then((cached) { const fetchPromise fetch(event.request).then((response) { if (response.ok) { const clone response.clone(); caches.open(CACHE_NAME).then((cache) cache.put(event.request, clone)); } return response; }); return cached || fetchPromise; }) ); });这段代码里最重要的是activate时的缓存清理。如果漏了这一步每次发版之后缓存都在膨胀而且用户永远拿到旧资源你排查问题又排查不到因为Service Worker的缓存跟HTTP缓存还不一样DevTools里清“清除缓存”不一定能清掉Service Worker的缓存得专门到Application面板里删。顺带说一句Service Worker的fetch事件拦截范围也包括跨域请求但跨域资源的cache.put有诸多限制最常见的就是不透明响应opaque response也可以缓存但状态码是0无法读取内容。如果你盲目把跨域资源缓存进Service Worker可能出现缓存了但无法验证内容的情况建议只对同源请求做完整的Network First策略。4. 常见问题与排查技巧实录4.1 发版后用户还在用旧资源三分钟定位这是我被问得最多的问题。按照下面的思路排查基本能解决九成的情况第一步看HTML有没有被缓存。打开线上首页打开DevTools的Network面板刷新页面找到index.html的请求。如果你看到状态码是200且Size列显示from memory cache或from disk cache说明HTML被强缓存了。正常应该是200且走网络或者304 Not Modified。这里插一句很多人会混淆200-from cache和304。强缓存命中时状态码是200但文件不是从网络回来的所以Size列会标注from cache协商缓存返回304时也是200不对304就是304Not Modified没有文件体。看到200 (from cache)直接定位到HTML缓存了。第二步看静态资源文件名有没有变化。发版后JS/CSS文件名里的hash应该变化。如果HTML里引用的还是旧hash文件名说明HTML文件本身没有更新服务器上大概率是旧文件——先检查部署流程是不是静态资源目录没有正确覆盖。如果HTML引用了新hash文件名但请求的还是旧URL那多半是浏览器缓存了旧的HTML。第三步看Cache-Control头。在Network面板里点开资源详情看Response Headers。如果静态资源配了max-age31536000但没有hash文件名那就会造成“内容变了但URL没变”的灾难。这里有且只有一个解法文件名必须带hash。4.2 缓存命中率分析与优化思路排查完问题之后我们要往优化方向走。缓存命中率这个概念分两块强缓存命中率和协商缓存命中率。强缓存命中率看的是Network面板里from cache的资源占比。如果想要更高优先确认静态资源是否都配置了长缓存和hash名字。Cherry-pick式的调整方式是把大图片、第三方库、框架运行时这些几乎不变的资源设置成长缓存把业务代码资源设置成中等缓存比如max-age86400并根据发版频率调整。协商缓存命中率看的是304请求占比。304依然有一次HTTP往返所以它不是零成本。如果发现某个资源的304比例极高说明它的文件内容很长时间没变了你可以考虑把它的Cache-Control从no-cache改成max-age86400甚至更长。这个调整需要测试不要拍脑袋改。DevTools里查命中率的另一个技巧是看请求头。如果请求头里有If-None-Match或If-Modified-Since说明这个资源正在走协商缓存路径如果什么都没有且状态200说明走的是网络全量下载。4.3 接口缓存与页面缓存的差异很多后端同学问过为什么我用fetch请求接口每次都能拿到最新数据但浏览器开发者工具里有时显示200 (from disk cache)这里要区分两件事。fetch请求也是普通HTTP请求它同样受缓存策略约束。浏览器的默认行为是隐式缓存GET响应不对准确说如果没有明确的Cache-Control响应头浏览器可能不会缓存也可能根据启发式规则缓存比如根据Last-Modified推断出一个新鲜期。这个“隐式缓存”非常坑因为它不在你的预期里但确实发生了。所以接口的缓存策略必须显式配置实时性要求高的接口Cache-Control: no-store每次强制请求。可以有短时缓存的接口比如首页聚合数据Cache-Control: max-age60。详情页、列表页等GET接口可以用协商缓存服务端根据ETag返回304。页面缓存和接口缓存还有一个本质区别页面缓存处理的是文档不只是文件内容接口缓存处理的是数据可能涉及权限差异。同一个URL如果不同用户看到的内容不同接口一定不能开强缓存协商缓存也要谨慎——除非服务端能把用户维度写进缓存键。4.4 一个我反复使用的调试技巧最后分享一个我个人一直在用的调试方法快速验证资源有没有走缓存。打开DevTools的Network面板勾选“Disable cache”。这个选项的作用是页面打开期间所有请求都不使用浏览器缓存每次都是完整请求。它和手动刷新不太一样手动刷新在Chrome里默认其实会复用部分缓存而勾选Disable cache是从根本绕开浏览器缓存方便你对比“有缓存”和“无缓存”两套请求行为。如果你要验证线上资源的实际缓存行为可以新开一个无痕窗口来测。无痕窗口默认没有旧缓存数据能帮你排除本地缓存干扰看清从零开始加载一个资源的完整链路。另外我不建议用强刷新CmdShiftR代替上面这两种方式来做缓存行为验证。强刷新会发请求头带上Cache-Control: no-cache实际上强刷新会让Chrome产生一个类似的“绕过缓存”的请求但它和正常用户的访问行为不一样所以看到的结果不能代表真实用户环境。4.5 缓存策略速查表资源类型推荐缓存策略关键配置HTML入口文件协商缓存Cache-Control: no-cacheJS/CSS带hash强缓存Cache-Control: max-age31536000, immutable图片/字体带hash强缓存Cache-Control: max-age31536000, immutable无hash的老接口数据短强缓存或协商缓存max-age60 或 ETag用户登录态/交易接口禁止缓存Cache-Control: no-store这张表我贴过很多次基本覆盖了一个常规Web项目的所有资源类型。照着这个表去配Nginx或者云服务商的CDN配置就能少踩大部分缓存坑。我自己在这些年的项目里攒下来一条最重要的心得别指望一个缓存策略适配所有资源。资源类型不同、时效要求不同缓存策略就该分门别类地配。一套裸配走天下的方案到最后一定会遇到“要么更新不及时要么性能上不去”的两难。花半天时间把缓存策略理清楚后面能省下无数次排查问题的功夫。
返回列表