# The app shell model

> How the app shell pattern caches a page's markup, styles, scripts, and images in a service worker so repeat visits render the UI chrome instantly.

**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

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

```js
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

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:

```js
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

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:

```js
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

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](/reference/performance/precaching/) for the broader precaching
APIs this pattern relies on.

## 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 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.

## Where to go next

- [Precaching](/reference/performance/precaching/)
- [Navigation preload](/reference/performance/navigation-preload/)