Vue 3 + PWA:头像明明命中缓存,为什么返回首页还会再次请求?
最近在优化一个 Vue 3 社区项目时,遇到一个很容易误判的问题:
- 头像已经显示过;
- 返回首页时几乎瞬间出现;
- 但 Network 面板里仍然会再次看到头像请求。
直觉上会怀疑组件被重新挂载、浏览器缓存失效,或者 CDN 没有命中。最后排查发现,资源其实已经进入 Service Worker 缓存,重复请求恰恰是缓存策略主动发起的。
一、先分清三层缓存
一个 PWA 页面里的静态资源,至少可能经过三层:
- 浏览器 HTTP 缓存:由
Cache-Control、ETag等响应头控制; - Service Worker Cache Storage:由 Workbox 或手写 SW 的策略控制;
- CDN/边缘缓存:例如 Cloudflare,决定请求是否回到源站。
看到from ServiceWorker,只说明页面拿到了 SW 返回的响应,不等于 SW 没有在后台联网。
二、根因:StaleWhileRevalidate 本来就会再请求一次
StaleWhileRevalidate的工作方式是:
请求资源 ├─ 缓存中有:立即返回缓存 └─ 同时发起网络请求:更新缓存,供下一次使用它很适合需要“先快后新”的接口或经常变化的图片。但对于头像、帖子图片等已经具备稳定 URL 或版本参数的资源,它会产生一个副作用:用户每次回到页面,内容显示得很快,却仍然会多一次真实网络请求。
旧配置可以抽象成这样:
{urlPattern:({url})=>url.pathname.startsWith('/uploads/')||url.hostname==='media.example.com',handler:'StaleWhileRevalidate',options:{cacheName:'media-cache'}}如果目标是“资源一旦缓存,后续尽量不联网”,更合适的是CacheFirst。
三、媒体资源改为 CacheFirst
在vite-plugin-pwa的 Workbox 配置中,可以按资源类型划分运行时缓存:
import{defineConfig}from'vite'import{VitePWA}from'vite-plugin-pwa'exportdefaultdefineConfig({plugins:[VitePWA({registerType:'autoUpdate',workbox:{runtimeCaching:[{urlPattern:({url})=>url.pathname.startsWith('/uploads/')||url.pathname.startsWith('/static/')||url.pathname.startsWith('/official/')||url.hostname==='media.example.com',handler:'CacheFirst',options:{cacheName:'app-media',cacheableResponse:{statuses:[0,200]},expiration:{maxEntries:300,maxAgeSeconds:30*24*60*60,purgeOnQuotaError:true}}}]}})]})这里有三个细节:
statuses: [0, 200]兼容同源响应和部分跨域 opaque response;maxEntries防止大量图片无限占用本地存储;purgeOnQuotaError允许存储空间不足时优先清理运行时缓存。
需要注意:CacheFirst不能单独解决资源更新问题。URL 永远不变时,用户可能长期看到旧图。因此必须配合版本化。
四、稳定资源要带内容版本
带哈希的构建资源天然适合长期缓存:
/assets/index-CYN-L7my.js /assets/PostDetail-D1abc234.css文件内容变化,文件名也变化,所以可以放心设置:
location ^~ /assets/ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable" always; }对于文件名固定的官方头像,也可以增加内容版本:
constOFFICIAL_AVATAR_VERSION='f2d13162c146'exportfunctionofficialAvatarUrl(path:string){return`${path}?v=${OFFICIAL_AVATAR_VERSION}`}更新头像时同步更新版本值,旧缓存自然失效,不需要让每次访问都重新校验。
五、入口文档和 Service Worker 不能长期缓存
不是所有资源都应该immutable。下面两个文件如果被长期缓存,可能导致用户一直停留在旧版本:
location = /index.html { expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate" always; } location = /sw.js { expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate" always; }推荐的整体分层是:
| 资源 | 策略 | 原因 |
|---|---|---|
| 带哈希 JS/CSS | 一年 immutable | 内容变化即换 URL |
| 头像、帖子图片 | SW CacheFirst + 有界缓存 | 回访时减少网络请求 |
| 官方固定资源 | URL 版本参数 + immutable | 兼顾长期缓存与主动更新 |
index.html | no-store | 及时拿到新构建入口 |
sw.js | no-store | 及时发现新 SW |
| 用户态 API | 不做公共缓存 | 防止数据串用和过期 |
六、别把“重复 API 请求”误认为静态资源问题
这次排查还发现另一个独立问题:顶部栏在watch(..., { immediate: true })中初始化了一次通知、私信和 WebSocket,随后又在onMounted中执行同一套逻辑。
典型反例:
watch(currentUser,initRealtime,{immediate:true})onMounted(()=>{initRealtime()// 重复初始化})可以保留单一入口,并让连接函数本身具备幂等性:
letsocketStarted=falseasyncfunctioninitRealtime(user?:User){if(!user||socketStarted)returnsocketStarted=trueawaitPromise.all([loadUnreadNotifications(),loadUnreadMessages()])connectWebSocket()}watch(currentUser,initRealtime,{immediate:true})另外,首页启动后无条件import()热榜、精华页,也会下载用户可能根本不会访问的 JS/CSS。预加载不是越多越好:它应该服务于高概率的下一步,而不是把所有路由提前请求一遍。
七、如何验证优化真的生效
建议按下面的顺序验证:
- 清理旧 Service Worker 与 Cache Storage,首次加载新版本;
- 访问包含头像或媒体图片的页面;
- 返回首页后再次进入;
- 在 Network 中确认资源来自 Service Worker;
- 确认没有由
StaleWhileRevalidate触发的后台媒体请求; - 直接请求 CDN 资源两次,检查第二次是否边缘命中;
- 更新资源版本,确认新 URL 能绕过旧缓存。
命令行也可以辅助检查响应头:
curl-I'https://www.example.com/assets/index-HASH.js'curl-I'https://www.example.com/sw.js'前者应适合长期缓存,后者则应允许浏览器及时重新验证或获取新版本。
总结
“页面显示很快但 Network 仍有请求”并不矛盾。StaleWhileRevalidate的设计目标就是先返回旧缓存、再后台更新。
这次优化最终遵循四条规则:
- 媒体资源使用
CacheFirst,并限制容量和有效期; - 长期缓存资源必须有哈希文件名或内容版本参数;
index.html与sw.js保持可更新;- 同时检查组件生命周期、路由预加载和 WebSocket 初始化,避免应用层重复请求。
缓存治理不是简单地把max-age调大,而是让“资源身份、更新方式和缓存策略”彼此匹配。
我正在把这类真实的开发与运营记录整理到 Zens。它是一个友善、真实、鼓励长期表达的开放社区;如果你愿意分享完整教程、独立作品或排障过程,可以来看看:
https://www.allinsong.top/?utm_source=csdn&utm_medium=technical_article&utm_campaign=zens_first_wave_202607&utm_content=pwa_cache
参考文档
- Workbox Strategies:https://developer.chrome.com/docs/workbox/modules/workbox-strategies
- MDN Cache-Control:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
- Vite PWA:https://vite-pwa-org.netlify.app/