静态站点离线缓存的失效治理与版本仲裁方案

举报
yd_282816122 发表于 2026/11/07 13:57:20 2026/11/07
【摘要】 背景痛点当我们给静态站点开启 Service Worker 离线缓存后,会遇到几个典型问题:资源更新后,部分用户依然长时间命中旧 SW,页面资源版本不一致(HTML 是新版,图片/JS 是旧版);直接删除 cacheName 缓存容易引发闪白、资源雪崩,大量并发重新拉取;没有版本标识,无法判断当前用户加载的是哪一版站点,线上问题难以复现;回滚发布后,旧 Service Worker 不会自...

背景痛点

当我们给静态站点开启 Service Worker 离线缓存后,会遇到几个典型问题:

  1. 资源更新后,部分用户依然长时间命中旧 SW,页面资源版本不一致(HTML 是新版,图片/JS 是旧版);
  2. 直接删除 cacheName 缓存容易引发闪白、资源雪崩,大量并发重新拉取;
  3. 没有版本标识,无法判断当前用户加载的是哪一版站点,线上问题难以复现;
  4. 回滚发布后,旧 Service Worker 不会自动切换,用户需要强制刷新甚至清缓存。

很多方案只做了“缓存”,没做好“缓存退役”。离线能力的难点从来不是能离线访问,而是可控地升级、可控地失效。

核心设计思路

整体思路分为四块:版本埋点、缓存命名隔离、仲裁更新策略、垃圾回收。

  1. 构建全局站点版本号,跟随 CI 构建自动生成(Git commit hash / build timestamp);
  2. CacheName 带上版本标识,不同版本缓存空间物理隔离;
  3. SW 启动时做版本仲裁,对比页面内埋点版本与 SW 自身版本;
  4. 对废弃的旧缓存空间做惰性清理,避免一次性大量删除阻塞主线程。

实现改造

1. 构建阶段注入版本号

在打包脚本里自动输出版本文件,同时注入到 HTML meta。

// build-version.js
const fs = require('fs');
const buildId = process.env.GIT_COMMIT || Date.now().toString();
const versionFile = `window.__SITE_VERSION__ = "${buildId}";`;
fs.writeFileSync('./dist/version.js', versionFile);

HTML 头部注入:

<meta name="site-version" content="__BUILD_VERSION__">
<script src="/version.js"></script>

2. Workbox 缓存名带上版本

// sw.js
import {registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate, CacheFirst} from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';

const SITE_VERSION = '__BUILD_VERSION__';
const CACHE_PREFIX = 'site-static';
const CACHE_NAME = `${CACHE_PREFIX}-${SITE_VERSION}`;

// 静态资源缓存策略
registerRoute(
  ({request}) => ['script', 'style', 'image', 'font'].includes(request.destination),
  new CacheFirst({
    cacheName: CACHE_NAME,
    plugins: [
      new CacheableResponsePlugin({statuses: [200]})
    ]
  })
);

// HTML 使用 stale-while-revalidate,优先展示缓存,后台更新
registerRoute(
  ({request}) => request.mode === 'navigate',
  new StaleWhileRevalidate({
    cacheName: `${CACHE_PREFIX}-html-${SITE_VERSION}`
  })
);

3. SW 安装激活时,清理旧缓存

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          // 不是当前版本、且属于本项目缓存前缀的,标记为待清理
          if (cacheName.startsWith(CACHE_PREFIX) && cacheName !== CACHE_NAME && cacheName !== `${CACHE_PREFIX}-html-${SITE_VERSION}`) {
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

4. 页面侧版本仲裁,优雅提示刷新

页面 JS,检测 SW 版本不一致:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(reg => {
    reg.addEventListener('updatefound', () => {
      const newWorker = reg.installing;
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'waiting') {
          // 发现新版本 SW 已经就绪,可以弹出提示让用户刷新
          const ok = confirm('站点已有新版本,是否立即刷新?');
          if (ok) newWorker.postMessage({type: 'SKIP_WAITING'});
        }
      })
    })
  })
}

在 SW 中监听 SKIP_WAITING 消息,跳过等待直接激活:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

策略取舍

  • ✅ 优点:版本隔离清晰,回滚时只要重新发布旧版本号,就能逐步覆盖用户;惰性删除不会一次性打爆带宽;可埋点统计线上版本分布。
  • ⚠️ 代价:多版本缓存会短暂占用浏览器存储空间;频繁发布会残留少量旧缓存,依赖激活阶段清理;不能做到毫秒级全站立刻更新,这是 SW 模型本身的限制。
  • ❌ 不推荐:全局固定 cacheName + 直接清空缓存,极易出现 HTML 和资源版本撕裂。

可观测与灰度扩展

  1. 上报版本信息:页面加载时把 __SITE_VERSION__、SW 状态(active/waiting/installing)上报到埋点;
  2. 灰度版本:可以增加路由判断,对部分用户下发新 SW,其余用户继续使用稳定版本;
  3. 紧急熔断:增加一个远程开关 JSON,SW 拉取开关,如果开关关闭,直接禁用缓存、全部走网络。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。