当前位置: 首页 > news >正文

Vue 3 + PWA:头像明明命中缓存,为什么返回首页还会再次请求?

最近在优化一个 Vue 3 社区项目时,遇到一个很容易误判的问题:

  • 头像已经显示过;
  • 返回首页时几乎瞬间出现;
  • 但 Network 面板里仍然会再次看到头像请求。

直觉上会怀疑组件被重新挂载、浏览器缓存失效,或者 CDN 没有命中。最后排查发现,资源其实已经进入 Service Worker 缓存,重复请求恰恰是缓存策略主动发起的。

一、先分清三层缓存

一个 PWA 页面里的静态资源,至少可能经过三层:

  1. 浏览器 HTTP 缓存:由Cache-ControlETag等响应头控制;
  2. Service Worker Cache Storage:由 Workbox 或手写 SW 的策略控制;
  3. 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.htmlno-store及时拿到新构建入口
sw.jsno-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。预加载不是越多越好:它应该服务于高概率的下一步,而不是把所有路由提前请求一遍。

七、如何验证优化真的生效

建议按下面的顺序验证:

  1. 清理旧 Service Worker 与 Cache Storage,首次加载新版本;
  2. 访问包含头像或媒体图片的页面;
  3. 返回首页后再次进入;
  4. 在 Network 中确认资源来自 Service Worker;
  5. 确认没有由StaleWhileRevalidate触发的后台媒体请求;
  6. 直接请求 CDN 资源两次,检查第二次是否边缘命中;
  7. 更新资源版本,确认新 URL 能绕过旧缓存。

命令行也可以辅助检查响应头:

curl-I'https://www.example.com/assets/index-HASH.js'curl-I'https://www.example.com/sw.js'

前者应适合长期缓存,后者则应允许浏览器及时重新验证或获取新版本。

总结

“页面显示很快但 Network 仍有请求”并不矛盾。StaleWhileRevalidate的设计目标就是先返回旧缓存、再后台更新。

这次优化最终遵循四条规则:

  1. 媒体资源使用CacheFirst,并限制容量和有效期;
  2. 长期缓存资源必须有哈希文件名或内容版本参数;
  3. index.htmlsw.js保持可更新;
  4. 同时检查组件生命周期、路由预加载和 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/
http://www.jsqmd.com/news/1178557/

相关文章:

  • iOS应用隐私合规实战:Guideline 5.1.2与ATT框架深度解析
  • 3个实用场景解析:如何用VideoDownloadHelper浏览器扩展高效获取在线视频资源
  • Krea2与ComfyUI人设图生成:从环境配置到高级工作流实战
  • C++调用WPS API实现Office文档自动化:免费、轻量、国产化替代方案
  • 回溯算法实战:从四后问题理解递归、剪枝与C++实现
  • UniApp中AES加密实战:从Crypto-JS到WebAssembly的高性能方案
  • Unity角色动画实战:Mixamo资源整合与Animator系统深度配置指南
  • TB67H480FNG与STM32F303VE的电机控制优化方案
  • A3910与STM32F401RE电机控制开发实战
  • QQ三国商行查询工具逆向实战:从CE定位到易语言HOOK封包解密
  • FastAPI核心原理:类型提示与Pydantic如何驱动校验、序列化与文档生成
  • Prompt重构失败?Perplexity精准检索失效真相,资深AI工程师的12条避坑清单
  • C++智能指针深度解析:从RAII原理到现代工程实践
  • 以人为本的AI如何改善健康结局:原理、价值与落地挑战
  • 模板驱动型文档自动化:从Word快照到规则引擎的范式升级
  • Linux学习之旅 | 用VMware Workstation安装Ubuntu
  • 隐函数求导原理与C++实现:从自动微分到工程应用
  • Python 学习笔记(第一期与第二期)——基础语法——核心知识点自测与详解
  • 开源!我用 Python 造了个 AI 视频生成框架,输入一句话自动出片
  • Nano Banana2:基于Gemini 3.1 Flash的AI图像生成工具实战指南
  • Unidbg+IDA逆向魔改SHA1算法:绕过反调试的SO模拟执行实战
  • 《造梦之家》电影技术解析:4K画质与手持拍摄的情感叙事
  • C++插件机制实战:从动态链接到工业级架构设计
  • C++矩阵转置性能优化:从基础实现到缓存友好与并行计算
  • Unity微信小游戏包体瘦身实战:代码剪裁与TMP字体优化指南
  • 思科交换机VLAN间通信:Access与Trunk端口配置的2个关键区别与1个常见误区
  • 幻兽帕鲁存档编辑完全指南:如何安全修改游戏数据实现个性化体验
  • Python自动化实战指南:从工具选型到工作流构建
  • JSON数组超10万条时Cursor智能跳转失灵?用这4个自定义Snippet+1个AST解析宏实现毫秒级字段定位
  • 《阿甘正传》深度解析:纯粹人生与历史隐喻的经典启示