性能 · 概念
预缓存策略
发布于
预缓存在 Service Worker 的 install 事件中、在任何页面请求之前,把一份构建时固定的文件列表存入 Cache Storage,于是应用外壳从首次访问起就在设备上,并且可以离线加载。它是运行时缓存的对应物,后者在响应被请求时才存储;两者配合使用:预缓存外壳,运行时缓存内容。
安装处理函数用这份列表调用 cache.addAll();任一条目失败则 Promise 拒绝、worker 不安装,这避免了半成品外壳上线。之后的每次部署都发布新列表。难点在于变更检测:URL 不变的文件(/index.html)即使内容不同,在缓存看来也一模一样,所以每个条目要么在 URL 里带内容哈希(/app.3f2a1c.js),让改动的文件成为新 URL,要么带一个独立的修订字符串供 worker 在安装时比较(workbox-precaching,developer.chrome.com)。手工维护修订号是最容易出错的一步,所以通常由构建插件生成清单。
什么属于预缓存
Section titled “什么属于预缓存”外壳:渲染框架的 HTML、它的 CSS 和 JavaScript 分块、图标和离线页。按用户或按请求变化的内容、大体积媒体,以及用户可能永远不会打开的东西,属于某种策略之下的运行时缓存(caching strategies overview,developer.chrome.com)。膨胀到数兆字节的预缓存会拖慢每次安装,在 Chrome 上还会因为 addAll() 争抢带宽而推迟首次访问自身的请求;典型应用的外壳只有几百 KB。
与 HTTP 缓存的相互作用
Section titled “与 HTTP 缓存的相互作用”安装期对每个条目的 fetch() 经过 HTTP 缓存。URL 不变却带着长 max-age 的文件,可能在修订号已变的情况下仍以过期副本进入预缓存;服务端规则是只给带哈希的 URL 设长 max-age,对 HTML 和 Service Worker 脚本本身这类无哈希的 URL 设 no-cache(需重新验证)(Service worker caching and HTTP caching,web.dev)。Workbox 把修订号作为 __WB_REVISION__ 查询参数附加到无哈希的条目上,以 cache: 'reload' 的语义绕过过期的 HTTP 缓存条目,而页面请求的 URL 不变。
预缓存只用到 caches.open()、cache.addAll() 以及 install 与 activate 事件,它们存在于 Chrome 40、Firefox 44、Safari 11.1 和所有现行引擎(BCD api.CacheStorage、api.ServiceWorkerGlobalScope)。Safari 对脚本可写存储的七天逐出会删除用户未交互站点的预缓存,之后的下一次访问会重新安装它;见逐出与尽力而为存储。
Workbox 形式是多数项目实际发布的;手写形式展示它在做什么。
使用生成清单的 Workbox precacheAndRoute
Section titled “使用生成清单的 Workbox precacheAndRoute”构建工具把 self.__WB_MANIFEST 替换为 { url, revision } 列表;带哈希的 URL 其 revision 为 null,其他为内容哈希。precacheAndRoute() 负责安装、提供和清理。
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
cleanupOutdatedCaches();precacheAndRoute(self.__WB_MANIFEST);// 构建时生成,例如:// [{ url: '/index.html', revision: '383676' }, { url: '/app.3f2a1c.js', revision: null }]cleanupOutdatedCaches() 移除旧版 Workbox 遗留的预缓存;当前预缓存自身的过期条目在 activate 期间移除,不需要它。
带修订号的手写预缓存
Section titled “带修订号的手写预缓存”不用 Workbox 时,同一机制就是一张 URL 到修订号的映射,加上只重新抓取变化条目的安装处理函数,第二次安装就不必重新下载整个外壳。
const PRECACHE = 'precache-v2';const MANIFEST = { '/index.html': 'a1b2c3', '/app.3f2a1c.js': null, '/offline.html': 'd4e5f6' };
self.addEventListener('install', (event) => { event.waitUntil((async () => { const cache = await caches.open(PRECACHE); for (const [url, revision] of Object.entries(MANIFEST)) { const key = revision ? `${url}?__rev=${revision}` : url; if (!(await cache.match(key))) { await cache.put(key, await fetch(key, { cache: 'reload' })); } } })());});fetch 处理函数对请求的 url 查找 ${url}?__rev=${revision};{ cache: 'reload' } 选项强制变化的条目绕过 HTTP 缓存走网络。
在依赖预缓存之前检测可用的 Cache Storage
Section titled “在依赖预缓存之前检测可用的 Cache Storage”页面可以判断外壳是否已预缓存,否则保持网络优先的行为,例如用来决定是否显示「可离线使用」的指示。
async function shellIsPrecached() { if (!('caches' in window) || !('serviceWorker' in navigator)) { return false; // 没有 Cache Storage:什么都没有预缓存 } const names = await caches.keys(); const precache = names.find((n) => n.startsWith('workbox-precache') || n.startsWith('precache-')); if (!precache) return false; const cache = await caches.open(precache); return Boolean(await cache.match('/index.html', { ignoreSearch: true }));}ignoreSearch: true 很重要,因为存储的键带着修订号查询参数;没有它,对 /index.html 的查找会落空。
- workbox-precaching(developer.chrome.com)
- Service worker caching and HTTP caching(web.dev)
- 应用外壳(app shell)模型
- CacheStorage 与 caches 全局对象
- Workbox
- 逐出与尽力而为存储
规范
| 规范 | 状态 |
|---|---|
| 无。 | |