Service Worker 缓存命中率怎么查?线上项目实战调优经验

上周帮客户排查一个 PWA 应用加载慢的问题,打开 DevTools 一看,Network 面板里大量请求都走了网络,明明注册了 Service Worker,缓存策略也写了 cache-first,结果缓存命中率只有不到 30%。后来发现,问题不在代码逻辑,而在几个容易被忽略的细节上。

缓存命中率不是“写了 cache.match 就自动有”

很多人以为只要在 fetch 事件里写了下面这段代码,缓存就稳了:

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});

但实际运行中,caches.match(event.request) 经常返回 null。为什么?因为匹配失败——URL 完全一致才匹配,参数顺序、大小写、尾部斜杠、甚至空格,差一点都不行。比如你缓存了 /api/user?id=123&type=detail,但页面发的是 /api/user?type=detail&id=123,就算内容一样,也查不到。

真实项目里怎么快速看命中率?

别靠猜,Chrome DevTools 里就有现成数据。打开 Application → Cache Storage,点开对应缓存名,右侧会列出所有已存资源;再切到 Network 面板,勾选 “Use large request rows”,然后刷新页面,观察每条请求的 Size 列:如果显示 “from cache”,说明命中;如果是具体字节数(比如 2.4 KB),就是走网络。手动数一遍,就能算出粗略命中率。

更省事的办法是加一行统计代码:

self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 跳过非同源、非 GET 请求(如跨域图片、POST 接口)
if (url.origin !== location.origin || event.request.method !== 'GET') {
event.respondWith(fetch(event.request));
return;
}

event.respondWith(
caches.match(event.request).then(cached => {
if (cached) {
// 命中缓存,发个自定义事件记录
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage({ type: 'CACHE_HIT', url: event.request.url }));
});
return cached;
} else {
return fetch(event.request).then(res => {
const cloned = res.clone();
caches.open('v1').then(cache => cache.put(event.request, cloned));
return res;
});
}
})
);
});

再配合前端监听页面 postMessage,就能实时汇总命中率,上线后还能连上报系统。

三个拉高命中率的实操技巧

1. 统一请求 URL 格式
在发起 fetch 前,对 query 参数做标准化排序。例如用这个小函数处理:

function normalizeUrl(url) {
const u = new URL(url);
const params = Array.from(u.searchParams.entries()).sort();
u.searchParams.delete();
params.forEach(([k, v]) => u.searchParams.append(k, v));
return u.toString();
}

缓存和匹配时都用 normalizeUrl 处理过的 URL,命中率立马提升一截。

2. 静态资源加版本哈希,动态接口单独缓存策略
JS/CSS/图片这类不变的资源,打包时加 contenthash,缓存名固定(如 static-v20240512),长期有效;而 /api/user 这类接口,建议用独立缓存(如 api-cache),并设置较短 max-age 或 stale-while-revalidate 策略,避免缓存过期数据影响体验。

3. 注意 CORS 请求默认不进缓存
如果你的 API 响应头没带 Access-Control-Allow-Origin: * 或明确域名,fetch 时加了 mode: 'cors',那即使缓存里有,caches.match 也会返回 null。检查响应头,或改用 no-cors 模式(注意:no-cors 下只能缓存 opaque 响应,无法读取内容,适合图片等只展示不解析的资源)。

天天顺科技最近给一家电商小程序做离线优化,把首页资源缓存命中率从 41% 拉到 92%,核心动作就是上面三条:URL 归一化 + 分层缓存 + CORS 响应头补全。没有黑科技,全是贴着业务踩出来的坑。