Module 2: Rendering strategies · Lesson 5 of 7 · 3 min read
App shell: cache the frame, stream the data
The static frame a service worker serves instantly, the data that fills it, and the size budget that keeps the frame small enough to be instant.
Two layers, two cache strategies
The App Shell doc splits an application in two. The static shell is the navigation, the layout grid, the critical CSS, the framework runtime and an offline fallback. The dynamic content is everything fetched from an API and rendered into that frame.
A service worker makes the split pay. On install it precaches the shell under
a versioned cache name (app-shell-v1) and then serves those files cache-first.
The next deploy installs app-shell-v2 and deletes the old cache on activation,
which is how an update reaches users without stale assets. Each resource type
gets its own strategy: shell assets cache-first with versioned URLs, API data
network-first with the cache as fallback, third-party scripts network-only.
The templates give you the pieces of a frame — the React template has a
Layout template under src/ui/templates and skeletons under
src/ui/skeletons — but none of them registers a service worker;
templates/chota-wc-saga/src/sw.js is an empty file. A cached shell is work you
specify, not a default you inherit.
What an agent gets wrong
The skill’s DON’T list reads like a review of a first attempt:
- personalised content baked into the shell — the shell is cached; content is what gets fetched per user
- API responses cached alongside the shell, under the shell’s strategy
- no offline fallback page
- render blocked on resources that are not critical
The doc adds three more: shell assets that are not cached aggressively (the whole benefit), a hydration mismatch when the shell is server-rendered, and every micro-frontend loaded up front when the first route needs one.
Budget the shell
“Instant” is a size claim, so give it a number. The practice question builds the
check: a performance-budget.json declaring a gzipped ceiling per entry point
and a smaller one for the critical path — the shell plus the first route.
Measure gzipped transfer size, not the file on disk. When the check fails, it
must name what grew (+38kb: moment@2.30 (locales)), because a total with no
cause gets answered by raising the budget. Raising it means editing the budget
file in the same pull request, so the increase is reviewed alongside the feature
that needed it. Start from today’s number plus a small margin; a budget copied
from a Lighthouse recommendation is red on day one and gets bypassed.
Brief it, then review it
Register a service worker that precaches the shell (index.html, the
hashed app bundle, critical CSS) under a versioned cache name and
serves it cache-first. Fetch /api/* network-first with a cache
fallback. Show the skeleton until data arrives. Add an offline page.
- The shell is cached under a versioned name, and old caches are deleted on activate.
- Nothing user-specific is in the shell.
- Skeletons show while content loads, and an offline fallback exists.
- The critical path sits under a declared, gzipped budget.
Encode it, then practise it
SKILL.md that encodes App Shell for a coding agent. Install it into your agent, or download it for any Agent-Skills-compatible runtime.
npx skills add grvpanchal/elegant --skill server-app-shell
Practise it
Guardrail a performance budget
A size budget that fails a change, attributes the growth to a dependency, and survives the week someone genuinely needs to exceed it.
Course contents (7 lessons)
Module 1: Routes and containers
Module 2: Rendering strategies
- SSR: HTML per request, and the hydration bill done
- SSG: render once at build time, cache it right done
- App shell: cache the frame, stream the data done
Module 3: The network edge