Skip to content

Performance · Concept

The app shell model

Published Updated

In one line: The app shell model caches the markup, styles, scripts, and images needed to render an app’s UI chrome in a service worker, so a repeat visit to an already-cached route can be answered from the cache instantly, before any content is fetched.

The app shell is the minimal HTML, CSS, and JavaScript powering a page’s UI — the parts of the interface that stay the same across routes (a header, navigation, layout skeleton) rather than the data that changes per page. developer.chrome.com describes caching this shell in a service worker and loading content into it separately; MDN separately documents that a service worker can cache general resources such as pages, styles, scripts, and images, which is the caching mechanism the app shell pattern builds on.

const SHELL_CACHE = 'app-shell-v1';
const SHELL_ASSETS = ['/', '/styles/app.css', '/scripts/app.js', '/icons/icon-192.png'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached ?? fetch(event.request))
);
});

Detecting service worker support and falling back

Section titled “Detecting service worker support and falling back”

This caching mechanism requires a service worker to intercept requests and serve the cache; without one, there’s no cache to answer from for this pattern specifically. developer.chrome.com notes sites can still fall back to plain HTTP caching (e.g. long-lived Cache-Control headers) for browsers without service worker support, rather than losing all caching:

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
} else {
// Fallback: no service worker support, so no offline-cached shell. Per
// developer.chrome.com, browsers here can still get a server-rendered page
// with far-future Cache-Control headers instead of this SW-based approach.
}

Per the Service Workers spec and developer.chrome.com, a service worker populates its cache during install and can use activate to explicitly delete outdated cache entries. Bumping the cache name (as in SHELL_CACHE above) so install populates a new cache, then deleting the old-named cache’s entries on activate, is how a deployed change to the shell’s markup, styles, or scripts reaches existing installs. Scope the deletion to the shell’s own cache-name prefix (app-shell-) so it only removes stale shell versions, not other same-origin caches such as a data cache or a runtime cache from an unrelated feature:

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key.startsWith('app-shell-') && key !== SHELL_CACHE)
.map((key) => caches.delete(key))
)
)
);
});

The app shell model is not a browser API itself — it’s a caching pattern built on the Service Worker API and the Cache API, both implemented across current Chromium, Firefox, and Safari. See Precaching for the broader precaching APIs this pattern relies on.

  • Cache only the shell’s own markup, styles, scripts, and images at install — not per-page content, which changes independently.
  • Feature-detect 'serviceWorker' in navigator and let the page load normally when it’s absent.
  • Name the cache with a version suffix, and delete old-versioned caches on activate when the shell’s assets change.
  • Populate content into the shell separately (network or a data cache) — don’t try to precache pages whose content changes per request.

Specifications

SpecificationStatus
None.