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.
What the shell is
Section titled “What the shell is”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.
Caching the shell at install time
Section titled “Caching the shell at install time”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.}Versioning the shell
Section titled “Versioning the shell”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)) ) ) );});Where it is supported
Section titled “Where it is supported”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.
Practical checklist
Section titled “Practical checklist”- Cache only the shell’s own markup, styles, scripts, and images at
install— not per-page content, which changes independently. - Feature-detect
'serviceWorker' in navigatorand let the page load normally when it’s absent. - Name the cache with a version suffix, and delete old-versioned caches on
activatewhen 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.