前端缓存实战:从HTTP缓存到Service Worker的性能提升方案

发布时间:2026/10/9 8:32:00
前端缓存实战:从HTTP缓存到Service Worker的性能提升方案 做前端这么多年被问得最多的问题之一就是“性能优化到底先做什么”我的回答一直很固定先把前端缓存整明白。缓存这东西不显山不露水却是最典型的“低投入高回报”优化项——不需要重构架构不需要引入重型监控只要把 HTTP 缓存头、浏览器存储、Service Worker 这几层策略调对首屏加载快 30% 不是口号而是能在性能面板里看到的真实数据。这篇文章就是冲着这个目标来的会从浏览器底层缓存机制讲起一路聊到 CDN、微前端、直播场景给你一套可以直接抄到项目里的缓存方案。不管你是准备面试的在校生还是已经在业务里扛性能优化的开发读完后应该能建立起一张完整的前端缓存坐标系。先说结论缓存不是简单的“存一下、读一下”它是一场围绕“数据新鲜度”和“读取成本”的博弈。下面我按自己的经验拆成几个层次来聊。1. 内容整体设计与思路拆解1.1 缓存解决的本质问题缓存的本质是消除重复劳动。一个用户访问你的站点加载了 200KB 的 JS关掉页面第二天再打开同一个 JS 又要重新下载一遍——这就是纯浪费。后端接口返回的数据、图片、字体、甚至组件渲染结果都存在同样的“重复计算”问题。我习惯把缓存类比成冰箱蔬菜从菜市场买回来网络请求放进冰箱缓存下次吃的时候直接从冰箱拿缓存命中不需要再跑一趟菜市场。但冰箱有个问题菜放太久了会坏对应到缓存就是数据过期。所以缓存设计要回答三个问题存什么、存多久、怎么知道它坏了。这三个问题的答案组合起来就是一套缓存策略。很多人只关心“存什么”忽略了“怎么失效”导致上线后用户看到旧页面最后只能靠清缓存解决问题这是典型的“只建机制不建失效”。1.2 前端缓存的地图一条请求的缓存之旅我把前端缓存放进一条请求链路里讲这样最好理解。你在浏览器输入网址按下回车静态资源的加载路径大概是这样的浏览器先查 Service Worker 有没有缓存有就直接返回这是离用户最近的一层。没命中就查内存缓存Memory Cache和磁盘缓存Disk Cache也就是所谓的 HTTP 缓存。本地没有才真正发出网络请求。请求先到 CDN 边缘节点如果 CDN 有缓存直接由 CDN 返回不到源服务器。CDN 没有才回源到你的 Nginx 或后端服务。后端返回时带着响应头告诉浏览器这个资源能不能缓存、缓存多久。页面开始解析 HTML、执行 JS这时候还能用上代码层面的缓存localStorage、IndexedDB、内存里的 Map、Vue 的 KeepAlive、React 的 memo。每一层缓存的存储位置、生命周期、控制手段都不同我整理了一张表方便对照缓存层存储位置生命周期控制方式典型场景Service Worker浏览器独立线程开发者控制Cache API、代码逻辑离线应用、精细缓存、体验优化Memory Cache浏览器内存短期随 tab 关闭清理HTTP 头 浏览器策略当前页面会话内资源Disk Cache磁盘长期按策略淘汰HTTP 头跨会话静态资源CDN Cache边缘节点由缓存头/平台规则控制Cache-Control、平台刷新静态资源加速本地存储浏览器存储永久/会话级JS API业务数据、用户状态代码级缓存内存对象页面生命周期代码逻辑计算结果、组件状态这张表是后面所有内容的总纲。你会发现每层缓存之间其实是嵌套关系设计缓存方案时要先画出来某个资源从用户点击到渲染会经过哪几层每层命中概率有多高失效粒度有多大。1.3 “性能提升30%”是怎么量出来的市面上很多文章谈“性能提升30%”但不讲清楚指标口径基本等于耍流氓。我先说明白我在项目里说的 30%主要看两个数字——首屏加载耗时和HTTP 请求数/传输体积。举个例子一个典型的 Vue 后台管理系统未做缓存优化时首屏大概要发 60 多个请求传输体积 1.8MB未压缩首屏完成时间 2.8 秒。做完静态资源缓存策略后二次访问时大部分请求从磁盘缓存命中真正走网络的请求从 60 多降到 10 个以内传输体积降到 200KB 左右首屏时间能压到 1.5~2 秒。这里面很大一部分功劳就是缓存第一次访问省不掉但用户又不是只访问一次。所以 30% 不是夸张关键是区分“首次访问”和“二次访问”。首次访问拼的是压缩、合并、CDN二次访问拼的就是缓存命中率。绩效报告里经常引用的是 Lighthouse 分数但 Lighthouse 默认是模拟首次访问它衡量缓存的权重有限。真要验证缓存效果要看 Network 面板里from disk cache和from memory cache的数量以及重复访问时的 LCP最大内容绘制时间。2. HTTP 缓存实战从强缓存到协商缓存这是整个前端缓存的底座也是面试里最高频的考点必须彻底吃透。2.1 Cache-Control 是强缓存的总开关HTTP 缓存里最优先看的是响应头Cache-Control。它的值是一组指令我一个个拆开说max-age31536000资源被缓存 1 年这一年里浏览器不会重新请求直接用本地副本。no-cache不是不缓存而是“使用前必须去服务器验证”走协商缓存流程。no-store真正的不缓存每次都要完整下载。public/privatepublic表示任何中间层CDN、代理都能缓存private表示只能浏览器缓存中间层不能缓存。immutable告诉浏览器这个资源在 max-age 内绝对不会变连刷新页面时都不用重新验证。实际配置里静态资源最常用的是Cache-Control: public, max-age31536000, immutable这里有个常见误区很多人以为设置了max-age就万事大吉但忽略了浏览器“刷新”行为。用户按 F5 刷新时就算强缓存没过期浏览器也会带着Cache-Control: no-cache去服务器验证一下。加了immutable后部分浏览器比如 Chrome 的资源子请求可以跳过这次验证对性能有一点帮助。不过immutable不能滥用只能用在带 hash 的文件名上。2.2 ETag 与 Last-Modified协商缓存的兜底方案就算设置了强缓存总有失效的时候。失效后如何确认资源“真的变了”这就要看协商缓存。协商缓存的流程是浏览器带着上次响应头里的标记发请求服务器比对后没变化就返回 304响应体为空浏览器继续用旧缓存有变化就返回 200 和新内容。常用的是两个标记Last-Modified文件最后修改时间精确到秒。缺点是没有秒以下精度且某些场景下修改时间没变但内容变了。ETag文件内容的指纹通常是 hash。只要内容变了ETag 一定变更靠谱。实践中我建议优先使用ETag。Last-Modified在 CDN 回源、动态生成资源时容易失真。真有静态资源场景可以两个都返回但浏览器优先走 ETag。关于 304 要强调一点协商缓存的最大价值不是“省流量”而是“省时间”。虽然 304 响应比 200 小很多但请求还是要经历完整的网络往返 RTT。所以从性能角度从上到下最优先的是让强缓存尽可能命中协商缓存只是兜底。2.3 工程化发布用文件名 hash 让缓存自动失效前端性能优化的核心矛盾是我想让静态资源缓存一年但代码每周都在改。怎么让浏览器知道“这个文件变了”答案是改文件名。现在主流构建工具都支持内容 hash 输出文件名。比如app.a3f0e8.jshash 是根据文件内容算的内容变了hash 就变文件名跟着变。这样你在服务器上可以放心地对所有带 hash 的资源设置immutable缓存一年因为内容变了浏览器自然会请求带新 hash 的文件旧文件在缓存里也没人用。催生了一个关键配置HTML 文件本身绝对不能强缓存。HTML 是“入口资源”它引用了哪些 JS/CSS 是靠文件名关联的。一旦 HTML 被强缓存浏览器拿着旧 HTML永远加载旧 hash 的 JS你就“白发布”了。所以 HTML 通常配置为Cache-Control: no-cache也就是“每次使用前都去服务器验证”。因为 HTML 体积小协商缓存返回 304 的成本完全可以接受。这一步我至少看到过五个项目栽跟头发布后用户反馈“页面没更新”最后查下来全是 HTML 被 CDN 或者 Nginx 缓存了。2.4 接入 CDN 后需要注意的缓存头前端项目部署基本绕不开 CDN。CDN 本质上是一堆分布在各地的边缘缓存服务器它的行为由你回源响应里的Cache-Control决定。但很多人不知道CDN 自己也有“缓存命中”和“回源”的概念。用 CDN 时最容易踩的坑是你在源服务器上的 Nginx 配好了Cache-Control但 CDN 平台还有独立的缓存规则两套规则叠加后不一定是你想要的结果。我遇到过的情况是Nginx 返回no-cache但 CDN 平台默认缓存了 HTML 2 小时导致发版后两小时内用户一直看到旧页面。正确做法是三层配合构建时给静态资源加 hash 文件名。源服务器 Nginx 对静态资源返回max-age31536000, immutable对 HTML 返回no-cache。在 CDN 控制台上对 HTML 路径单独设置“不缓存”或“跟随源站”对静态资源目录开启“跟随源站”缓存。每次发版后如果很重要可以在 CDN 控制台做一次“刷新预热”把 HTML 和入口文件缓存主动失效。这个操作在发布脚本里用 CDN 平台的 API 自动执行比手动点控制台靠谱得多。3. 浏览器端缓存手段内存、磁盘与本地存储HTTP 头控制的是“资源级别”缓存浏览器内部还有一套属于自己的缓存调度逻辑。理解这些才能在 Network 面板里看懂“from disk cache”和“from memory cache”到底怎么回事。3.1 Memory Cache 和 Disk Cache 是怎么选的这两个不是由前端代码控制的而是浏览器基于资源类型、大小、HTTP 头、当前内存压力做的自动决策。但它们的差异很重要Memory Cache存在内存里读取速度极快几乎为零开销。但生命周期短一旦关闭页面或者内存占用高就会被清掉。Disk Cache存在磁盘上跨会话保留读取速度相对慢一些但持久稳定。浏览器的选择逻辑大致是JS、CSS、图片等资源如果体积不大且当前页面还开着走 Memory Cache如果体积大或者浏览器觉得内存不够用就存到磁盘。还有一个细节在 Chrome 里跨 tab 复用的资源更可能走 Disk Cache。实际操作中我不太纠结这两者的调度因为咱们控制不了。我更关注的是在 Network 面板里确认“这个资源到底有没有命中 HTTP 缓存”。如果看到请求行里标注了from disk cache或from memory cache说明强缓存生效了网络请求根本没有发出去这就是性能提升的直接证据。3.2 localStorage、sessionStorage、IndexedDB 怎么选如果是业务数据比如用户配置、搜索历史、草稿箱内容就不能依赖 HTTP 缓存了要用浏览器的本地存储 API。这里我直接给选型结论特性localStoragesessionStorageIndexedDB容量约 5MB约 5MB可以到几百 MB 甚至更多存储类型字符串字符串结构化对象、二进制生命周期永久标签页关闭即清永久可手动清理API 复杂度同步简单同步简单异步复杂但功能强典型场景主题、token、简单配置表单临时状态复杂业务缓存、离线数据个人经验是能塞 localStorage 的不碰 IndexedDB因为 IndexedDB 的异步 API 会显著增加代码复杂度。只有数据量大、结构复杂比如缓存金字塔模型数据、上报队列才上 IndexedDB。有个很经典的坑localStorage 只能存字符串很多人直接存对象结果隐式调用了toString()存进去变成[object Object]读出来全废。必须用JSON.stringify和JSON.parse包裹。另外localStorage 是同步读写如果在一个大循环里频繁操作会造成主线程阻塞影响页面流畅度。正确的做法是批量读写或者把高频操作挪进 IndexedDB 的异步环境。3.3 代码级缓存与组件缓存Vue KeepAlive 和 React memoHTTP 缓存管的是网络层面代码层面还大有文章可做。面试题里经常问到 Vue 的KeepAlive和 React 的memo这两个东西本质上就是缓存。Vue 的KeepAlive组件会缓存组件实例切换路由或 v-if 切换时组件不会销毁重建而是从缓存里直接激活。代价是组件占用的内存不会被释放所以只适合缓存“创建成本高但切换频繁”的页面比如列表页切详情页再返回列表滚动位置能保留。我用它缓存过一个数据量很大的报表页面切换耗时从 800ms 降到了 100ms 左右体验提升非常明显。React 的React.memo和useMemo是另一种思路缓存“渲染结果”和“计算值”。React.memo包裹组件后父组件重新渲染时只要 props 的引用没变子组件就跳过渲染。useMemo则缓存昂贵计算的返回值避免每次渲染都重算。但这类缓存必须小心使用依赖数组写错会导致意想不到的 bug。我踩过最痛的一次是useMemo里漏了依赖项整个模块跑出了错误数据排查了整整一下午。还有一个容易被忽略的代码级缓存前端组件库。不少组件库的样式和主题对象会在运行时被重复计算。如果项目里树摇优化做得好配合 Vite/Webpack 的模块缓存组件库构建时间能缩短 40% 以上。这不算运行性能但开发体验同样是“性能”。3.4 登录态与 token 的缓存细节登录状态是 localStorage 用得最多的场景。移动端 H5、uni-app 一类的项目里用户登录后的 token 通常存 localStorage请求时塞进 header。这里有几个细节你最好注意token 存 localStorage 有 XSS 风险如果页面被注入脚本token 会被偷。敏感项目里优先考虑把 token 放内存变量或者用httpOnlycookie 方式但代价是更复杂。token 的“有效期”和“缓存有效期”是两码事。我见过有人在 localStorage 里存了 token但服务端会话早已过期前端一直用旧 token 请求直到被 401 弹一脸。标准做法是请求拦截器里对 401 做统一处理清掉本地 token 跳登录页。登录状态在多标签页之间会不一致。某 tab 登出了另一个 tab 的 localStorage 不会自动更新。这时候靠storage事件监听其他 tab 只要监听到 key 变化就同步更新全局状态。这是一个很容易被忽略的“缓存一致性”问题和后面要聊的缓存过期主题一脉相承。4. Service Worker 与 Cache API离线可用和控制力拉满HTTP 缓存虽然好用但能控制的粒度有限它只能按“资源”去缓存策略主要由响应头决定。如果你想实现“页面优先用缓存后台悄悄更新”“离线时也能打开应用”就必须请出 Service Worker。4.1 Service Worker 的工作原理与生命周期Service Worker 是浏览器里的一个独立线程它位于所有网络请求之前相当于一个“中间人代理”。页面发出的请求会先被 Service Worker 截获由你写的代码决定直接返回缓存、转发到网络、或者两者同时做。Service Worker 不能直接用要理解它的生命周期注册页面 JS 调用navigator.serviceWorker.register(/sw.js)。安装浏览器下载并安装sw.js触发install事件这时候适合预缓存静态资源。激活旧的 Service Worker 被替换后触发activate事件这里适合清理旧缓存。运行此后所有受控页面的请求都会触发fetch事件。开发时最难受的一点是Service Worker 的更新不像普通 JS 那么即时。sw.js本身的更新频率受浏览器控制默认情况下每次导航最多检查一次而且有 24 小时的强制更新上限策略。我在本地调试时经常改了sw.js不生效必须在 DevTools 的 Application 面板里勾选 “Update on reload” 才能每次刷新都更新。这个坑说大不大但第一次遇到会非常困惑。4.2 三种核心缓存策略与选择Service Worker 里最常用的策略是三个我直接给适用场景Cache First缓存优先有缓存就直接返回不请求网络。适合那些版本固定、不经常变的静态资源比如图片、字体、带 hash 的 JS/CSS。速度最快但需要配合版本管理。Network First网络优先先请求网络失败或超时后再用缓存兜底。适合对实时性有要求的接口返回但想要离线兜底。首页 HTML 也可以用它。Stale-While-RevalidateSWR先用缓存立即返回同时后台发起网络请求成功后更新缓存。这是体验和数据新鲜度的折中方案。比如用户头像、商品列表这类“可以看旧数据但最好有新数据”的场景。我自己的项目里很少只用一种策略通常按路由路径分/api/前缀的接口走 Network First/static/前缀走 Cache FirstHTML 文档走 SWR。这套组合拳能在“快”和“新”之间取得平衡。4.3 版本管理与更新避免用户永远用旧包Service Worker 最大的坑是一旦用户装了你的 SW你发布的任何新 JS/CSS 都可能被它“拦截”掉导致用户永远用旧版。我的做法是给缓存名加版本号。比如const CACHE_VERSION v20250214; const CACHE_NAME my-site-${CACHE_VERSION};每次发版时手动更新版本号在activate事件里把所有不是当前版本的缓存全部删除self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((keyList) { return Promise.all( keyList .filter((key) key ! CACHE_NAME) .map((key) caches.delete(key)) ); }) ); });有了版本管理老用户虽然还运行着旧的 SW 逻辑但新页面首次访问会触发新 SW 安装旧缓存被清理。需要提醒的是旧的 SW 不会立刻接管网络浏览器要等当前页面全部关闭后才会让新 SW 完全掌权。所以大版本更新时建议提示用户“刷新一次”或者干脆在前端代码里做一次skipWaiting控制。这个机制比较反直觉不是“我发布完代码就立刻生效”的模型。4.4 直播 H5 的缓存取舍有读者问过“直播的前端 H5 怎么做”这里面也涉及缓存而且是个非常典型的“缓存和实时性冲突”场景。直播 H5 里页面容器、播放器 JS、UI 组件这些静态资源可以正常走缓存但直播流本身、弹幕、实时互动数据绝对不能走普通缓存。实际处理时我会分两类静态资源播放器 SDK、样式、页面骨架用 Cache First 或强缓存让二次进入直播间秒开。动态数据直播状态、房间信息、礼物列表用 Network First并且设置短的no-cache或者请求级缓存控制避免中间层缓存造成直播状态延迟。还有一个偏冷门但重要的点WebGL 直播场景里的着色器程序编译结果其实也有缓存一般由浏览器自动管理。如果用户第一次进直播间发现画面卡第二次明显好转除了 CDN 预热之外很可能是着色器缓存类似于常说的 GPU 着色器缓存大小和网络缓存在共同起作用。这里业务代码能做的有限但至少不要把 WebGL 上下文随便销毁重建重建意味着着色器要重新编译性能会打折扣。5. 缓存一致性最容易翻车的环节缓存不是孤立存在的。后端有 Redis、MyBatis、Spring 三级缓存前端也有自己的缓存体系。后端的缓存治理里常念叨的“穿透、击穿、雪崩”前端同样会遇到只是形态不一样。5.1 缓存穿透、缓存击穿、缓存雪崩的前端对照后端面试题里这三个词出现频率极高我先把它们解释清楚缓存穿透查询一个一定不存在的数据缓存里没有数据库里也没有每次请求都打到数据库。前端类似场景请求一个不存在的资源路径或者构造了一个永远不会命中的缓存 key每次都发真实请求。缓存击穿某个热点 key 突然失效大量并发请求同时打到后端。前端类似场景某个关键脚本的强缓存刚好过期刚好又有一大批用户同一时间刷新页面。缓存雪崩大量 key 在同一时段失效导致后端压力瞬间翻倍。前端类似场景一次性把所有静态资源的 max-age 改成固定值结果这些资源在同一个时间点集体到期回源请求打满源站。前端应对方法其实很直接穿透设置合理的过期时间或者对不存在的资源统一返回短时间缓存别让“空响应”也变成无底洞。击穿热点资源不要设统一的过期时间构建工具输出时加随机 hash 能天然错开或者用 SWR 策略先返回旧缓存再后台更新。雪崩给缓存过期时间加一个“随机偏移量”比如max-age31536000 随机数秒让资源过期时间散开。虽然 HTTP 头里 max-age 是服务端定的但你可以对不同目录配置不同时长。5.2 前后端缓存一致性怎么做这是很多业务系统的痛点后端更新了数据前端缓存还是旧的。治标不治本的办法是让用户手动刷新正规的办法有以下几种版本号机制接口返回的数据带一个version字段前端发现版本号变大时主动清理并重新拉取。这是最简单可靠的方式。ETag 机制请求时带上缓存的 ETag服务端比对后返回 304 或新数据。本质上是把 HTTP 协商缓存用到业务接口上。推送失效用 WebSocket 或 SSE 推送“数据已更新”的消息前端收到后清掉相应缓存。适合实时性要求高的场景比如后台配置发布、用户权限变化。stale-while-revalidate 思路前端先展示旧数据后台拉新数据成功后替换并更新 UI。这个思路不仅适用于 Service Worker也适用于业务数据的Map缓存。我说个真实场景一个内容管理后台发布文章后列表页面一直用 localStorage 缓存了列表数据结果编辑发布后列表不更新支撑同事跑来问是不是系统坏了。这就是典型的“缓存一致性没设计好”。后来我在请求列表时加了一个timestamp参数发布成功后清理对应 key问题彻底解决。大多数缓存 bug 不是缓存本身的问题而是没有定义“失效时机”。5.3 微前端 qiankun 的缓存隔离与共享现在很多团队用 qiankun 做微前端。微前端的缓存问题比单应用更头疼因为“子应用”之间既要做资源隔离又要尽量复用公共依赖。在 qiankun 架构下主应用加载子应用的 HTML、JS、CSS每个子应用都是独立部署、独立发版。这里有几个缓存相关的实操建议子应用的静态资源也要走“hash 文件名 immutable”的套路避免子应用发版后旧 JS 被缓存。qiankun 默认会把子应用资源加载到内存中执行子应用实例默认不会重复执行。如果你用loadMicroApp手动加载同一个子应用可能出现状态被复用的“假缓存”现象——打开两个子应用实例却共享了同一个状态。公共依赖比如 React、Vue、工具库可以由主应用统一预加载并通过externals让子应用跳过下载这相当于在“依赖层面”做了一次全局缓存切换子应用时公共资源不重新下载性能提升非常可观。做微前端时我建议把“缓存策略”提升到架构层面统一规划。因为子应用之间的资源存在“重复下载”问题如果不做公共依赖缓存子应用越多首屏越慢这部分性能优化是架构级红利。5.4 大文件上传中的分片缓存与断点续传热搜词里有一条“前端使用 worker 上传大文件”这个场景也绕不开缓存。上传大文件时常见的策略是用 Worker 做分片每传完一个分片就记录进度中断后从断点继续上传。这里的“进度记录”本质上是缓存。我给出的实用方案是文件分片后把每个分片的唯一标识例如文件 MD5 分片序号和上传状态存到 IndexedDB。上传中断后用户再次选择同一个文件先查 IndexedDB 里的分片状态已经上传成功的分片直接跳过。用 Web Worker 在后台做分片 hash 计算和上传避免主线程卡顿。这里要特别注意IndexedDB 的容量不是无限的大文件的分片元数据累积起来也占空间。项目里我通常会加一个“上传记录最大条数”限制比如只保留最近 20 条记录超出就删最旧的。缓存不是越多越好有节制才有质量。6. 提升30%的缓存落地手册前面原理聊了不少这里给一套可复制的落地方案从诊断到配置再到验证。6.1 用 Lighthouse 和专业面板定位缓存问题优化前先做诊断。我每次接手性能优化第一步从来不是改代码而是先打开 DevTools 的 Network 面板和 Lighthouse拿到“现状数据”。具体操作路径是打开页面Network 面板勾选 “Disable cache” 模拟首次访问加载完成后记录 LCP、请求数、传输体积。取消勾选 “Disable cache”再次刷新页面记录第二次加载的数据。对比两次结果。如果第二次加载的请求数大幅减少、总耗时明显降低说明正常缓存已经生效。如果两次差异不大重点检查 HTML 和静态资源的Cache-Control响应头有没有正确返回。Lighthouse 里有一个专门的 “Serve static assets with an efficient cache policy” 审计项它会直接列出哪些资源没设缓存头、缓存时间多长。这个审计项是优化效果最明显的清单照着改就行。6.2 一套可直接抄的缓存配置以 Nginx Webpack/Vite 构建产物为例核心配置可以这样写。构建侧确保输出文件名带 content hash。Vite 默认会为 JS/CSS 生成带 hash 的文件名Webpack 需要在output.filename里配置[contenthash]。Nginx 静态资源配置location /static/ { add_header Cache-Control public, max-age31536000, immutable; try_files $uri 404; } location /index.html { add_header Cache-Control no-cache; try_files $uri 404; }如果还接入了 Service Worker可以把sw.js文件单独设置location /sw.js { add_header Cache-Control no-cache; }因为sw.js如果被强缓存它自己就永远无法更新了这是很多人会忽略的致命细节。6.3 实测优化前后的数据样板为了让 30% 这个数字更具体我给一个自己的优化记录来自某后台管理系统数据已脱敏优化动作静态资源加 content hash设置immutable一年缓存。HTML 设置为no-cache。引入 Service Worker对静态资源做 Cache First。公共依赖改成 CDN 引入并设置长期缓存。优化后二次访问的对比指标优化前优化后首屏请求数589传输体积1.7MB180KB首屏耗时二次访问2.4s1.6sLCP二次访问2.1s1.4s这个结果里请求数和传输体积的下降立竿见影首屏时间下降约 33%。注意首次访问提升幅度没有这么大但“二次访问”才是用户真正高频体验的场景。这就是为什么我一直强调缓存优化是对“回访用户”的体验投资。6.4 几个必须避开的过度缓存陷阱缓存虽好但别“贪杯”。我总结几个踩过的坑接口数据不要随便塞 localStorage。很多后端数据有权限边界用户 A 的缓存被用户 B 看到就是安全事故。缓存之前先问一句这个数据是不是“所有人通用”。不要对所有资源都用 365 天强缓存。HTML、配置文件、运行时配置这类可能会动态变化的资源必须留验证通道。Service Worker 不要缓存跨域资源时忽略 CORS 限制。默认情况下fetch跨域请求如果响应头没有Access-Control-Allow-Origin放进caches.put()会直接抛错。要缓存跨域资源必须确保响应带有正确的 CORS 头或者用mode: no-cors缓存 opaque 资源但后者读不出内容只能整体复用。别把 user 相关的数据缓存成全局静态资源。曾经有个项目把当前用户信息写进了签名后的 JS 文件名里导致不同用户请求不同 hash 的文件缓存形同虚设。缓存设计要考虑“缓存粒度”公共资源按公共策略私有数据按私有策略。7. 常见问题与排查技巧实录真到了线上出问题排查流程比原理更重要。这里挑几个高频问题讲透。7.1 改动不生效怎么区分强缓存和协商缓存“我改了代码为什么线上还是旧版”这个问题我每周都能在群里看到。走查思路如下先打开 Network 面板看请求是from disk cache还是304。如果是from disk cache说明强缓存命中浏览器根本没有发请求。要么文件名 hash 没变构建侧的问题要么响应头配了max-age且未过期。如果是304说明协商缓存生效。304 本身没问题但要确认 ETag 是否真的变了。如果 ETag 因为构建工具配置不当“每次构建都变”那 304 有可能升级成 200 重新下载缓存就白做了。如果是200说明缓存完全没生效检查响应头里Cache-Control有没有被 CDN 覆盖。检查最直接的手段地址栏输入 URLNetwork 面板右键 “Clear browser cache”或者命令行测试curl -I https://your-site.com/static/js/app.3e9f2.js看返回头里有没有cache-control: max-age31536000, immutable。7.2 Nginx/CDN 缓存没刷新的排查路径发布后发现 CDN 节点还在返回旧资源排查顺序是先直接访问源服务器绕过 CDN看源站返回的资源是不是新版。如果源站是新版、CDN 是旧版说明 CDN 缓存没失效。去 CDN 控制台做一次目录刷新或 URL 刷新。如果你的部署脚本没做自动刷新要排查脚本里有没有调用 CDN API。没有的话下次发版必须补上。如果 CDN 刷新了还是旧版可能就是源站 Nginx 的Cache-Control配置不对CDN 拿不到更新指令。这类问题最怕“没有版本概念”资源 URL 不变CDN 只能靠时间淘汰。所以我在任何静态资源部署里都强制执行“文件名带 hash”规则从根上杜绝大部分 CDN 缓存未刷新问题。7.3 面试高频缓存题速查结合热词里的“前端面试题”我把缓存相关的最高频问题整理成一个速查表回答时按下面思路组织即可问题核心回答要点HTTP 缓存机制有哪些强缓存、协商缓存Cache-Control、Expires、Last-Modified、ETagETag 和 Last-Modified 区别精度、内容指纹 vs 时间戳、变化场景为什么静态资源要带 hash内容变化导致文件名变化绕过强缓存浏览器缓存了旧数据怎么破响应头、版本号、SW 更新、CDN 刷新Service Worker 有什么用拦截网络请求、离线缓存、缓存策略可编程什么是缓存穿透/击穿/雪崩对应不存在 key、热点 key 失效、批量 key 同时失效各有应对方案前端怎么实现 LRU 缓存用 Map 实现键值有序存储超过容量删除最久未使用的项缓存和 Cookie 的区别缓存是资源复用Cookie 是状态传递存储位置、大小、作用目标不同其中手写 LRU 缓存是近两年大厂的高频手撕题。它的思路很好理解用Map保存 key-value访问一个 key 时先删掉再重新插入保证 Map 的迭代顺序是最新访问的在最后插入时如果超过容量删除 Map 的第一个 key最久未使用。这个题目对应真实的缓存淘汰策略和浏览器磁盘缓存清理逻辑也相通。7.4 一个日常开发中的缓存调试小技巧最后分享一个很实用的小技巧如果你只想单独验证某个资源的缓存又不想清空全部缓存可以在 Network 面板里右键该请求选择 “Clear browser cache” 只清这一个想屏蔽某个资源走缓存可以用 DevTools 的 “Block request domain”或者临时改请求 URL 加一个?vtimestamp参数让浏览器把它当成全新资源。这个方法在排查“这个资源到底是不是缓存导致的问题”时特别有效率。我在工作中大部分缓存问题定位都靠这套“最小化复现”的思路先确认是否命中缓存、命中哪一层缓存再决定改哪一层配置。我个人做了这么多年性能优化最大的体会是缓存最容易出问题的不是技术实现而是“你以为你懂了”。每一种缓存都有自己的生命周期和失效条件组合起来就变成一个状态机。所以每上一个缓存策略先想清楚三句话缓存什么缓存多久怎么失效想清楚这三件事30%只是及格线真正受益的是后面省下来的大量排障时间。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询