跳转到内容

性能 · 概念

应用外壳(app shell)模型

发布于 更新于

应用外壳模型把 PWA 分成两部分:外壳,即绘制应用框架所需的最小 HTML、CSS、JavaScript 与图标,由 Service Worker 在安装时预缓存,并在之后的每次导航中从 Cache Storage 提供;内容,在外壳渲染之后再抓取或从数据缓存读取。结果是首次访问之后的任何一次访问,首次绘制都不依赖网络。

外壳就是跨路由完全相同的部分:页眉、导航、布局骨架,以及把内容渲染进去的脚本。它只在部署时变化,所以可以在构建时列出并在 install 事件中缓存;它体积小,所以安装能在一次访问内完成。随后 fetch 处理函数用缓存的外壳文档应答导航请求,页面自己的脚本再抓取路由数据,于是浏览器在内容的第一个字节到达之前就画出了框架(The App Shell Model,developer.chrome.com)。

外壳缓存以版本命名(app-shell-v3)。新部署改变名称,新 Service Worker 的 install 填充新缓存,其 activate 删除名称以外壳前缀开头但不等于当前名称的缓存。把删除限定在前缀内,运行时缓存和数据缓存(articles、images)不受影响。在 activate 运行之前旧 worker 继续提供旧外壳,所以除非 worker 调用 skipWaiting() 和 clients.claim(),部署要到第二次加载才可见。

外壳优先的架构框架快、内容慢:首次访问时外壳尚未缓存,之后的每次访问内容仍需一次请求,所以 Largest Contentful Paint 取决于外壳之后路由数据到达得多快。服务端渲染的 HTML 配合 NetworkFirst 或 StaleWhileRevalidate 策略在首次访问就给出完整的首次绘制,代价是重复访问要等网络。许多应用两者并用:离线导航用预缓存的外壳,在线时提供服务端渲染的页面。外壳还需要一个离线兜底页来应对内容未缓存的路由,否则框架会围着一个错误渲染。

该模型是建立在 Service Worker API 与 Cache Storage 之上的模式,两者存在于 Chrome 40、Firefox 44、Safari 11.1 以及所有现行引擎(BCD api.ServiceWorker、api.CacheStorage)。没有 Service Worker 时外壳仍作为普通页面配合 HTTP 缓存加载,所以该模式退化为普通站点而不是失败。

下面的 worker 代码就是整个模式;页面侧的代码只负责注册它。

安装步骤缓存外壳文档和资源;fetch 处理函数用外壳应答导航请求,其他请求缓存优先、网络兜底。

const SHELL = 'app-shell-v3';
const SHELL_ASSETS = ['/shell.html', '/styles/app.css', '/scripts/app.js', '/icons/icon-192.png'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(SHELL).then((cache) => cache.addAll(SHELL_ASSETS)));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k.startsWith('app-shell-') && k !== SHELL).map((k) => caches.delete(k)))
)
);
});
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(caches.match('/shell.html').then((shell) => shell ?? fetch(event.request)));
return;
}
event.respondWith(caches.match(event.request).then((hit) => hit ?? fetch(event.request)));
});

event.request.mode === 'navigate' 是顶级导航的判据;作用域下的每个路由都拿到同一个外壳,客户端路由读取 location.pathname 决定加载什么内容。

注册并在没有 Service Worker 时退回

Section titled “注册并在没有 Service Worker 时退回”

页面在 API 存在时注册 worker,否则什么都不做,站点就以配合 HTTP 缓存的服务端渲染页面继续工作。

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch((err) => console.warn('没有外壳缓存:', err));
} else {
document.documentElement.dataset.shell = 'network'; // 没有 Service Worker:普通页面加载
}

data-shell 属性让 CSS 或分析代码无需其他改动就能区分两种模式。

规范

规范状态
无。