App Shells are the per-route prerenders of the parts of a page that do not depend on URL data. With Cache Components enabled, Next.js builds these reusable frames ahead of time, so even a dynamic URL that was never listed at build time opens with an instant first paint.
The shell is not a file you create. It falls out of how you structure the route.
App Shells come from Cache Components, enabled with cacheComponents: true. Serving one for a URL whose params were never listed at build time works from Next.js 16.3; earlier versions wait for a full server render before sending the response. Upgrading that shell to a full route on prefetch is the job of partialPrefetching: true, which is opt-in and requires cacheComponents.
What ends up in the shell
Three kinds of output join the shell. Static markup that uses only build-time data always goes in. Cached data goes in when its lifetime is long enough, specifically when the stale time is at least five minutes, because the shell is reused longer than short-lived content stays fresh.
Suspense fallbacks go in too, marking where the rest will stream.
| Shell content | Included when |
|---|---|
| Static markup | Always |
| Cached data | stale time is 5 minutes or more |
| Suspense fallbacks | Always |
Anything that reads URL data or per-visitor data stays out and streams in at request time.
Keep params inside Suspense
The structure that produces an App Shell is a layout that does not await params at the top level. It passes the params promise into a Suspense boundary instead.
// app/[category]/layout.tsx
import { Suspense } from 'react'
type LayoutProps = { children: React.ReactNode; params: Promise<{ category: string }> }
async function CategoryHeader({ params }: Pick<LayoutProps, 'params'>) {
const { category } = await params
return <h1>{category}</h1>
}
export default function CategoryLayout({ children, params }: LayoutProps) {
return (
<div>
<Suspense fallback={<h1>Loading category...</h1>}>
<CategoryHeader params={params} />
</Suspense>
{children}
</div>
)
}Because the await happens inside the boundary, the layout itself never waits for the URL. For a known category the heading prerenders into the page. For an unknown category, the fallback sits in the shell and the heading streams in later.
Awaiting params above the Suspense boundary would tie the shell to one URL and break this behavior.
Session shells
A shell that reads cookies() or headers() is session-specific. It still ships instantly, but it is cached per session on the client rather than in the shared server cache, so different visitors get different shells without leaking state between them.
You do not configure this. Next.js detects the session read and switches that shell to per-session client caching on its own. The practical payoff is that session-derived UI, such as a signed-in account menu, can sit in the shell instead of waiting behind a fallback on every navigation.
App Shell and ISR
The shell is the starting point, not the final page. When someone visits a URL whose params were never prerendered, Next.js serves the App Shell instantly and then upgrades it in the background with the now-known params.
The next visitor gets the concrete page from the cache. That upgrade path is Incremental Static Regeneration, and you decide which URLs skip the shell entirely by listing their param values in generateStaticParams.
A prefetch counts as that first visit, so a link that enters the viewport can start the background upgrade before the click. For the full render pipeline behind the shell, see how Partial Prerendering works.
Rune AI
Key Insights
- An App Shell is the URL-independent prerender of a route.
- Static markup and cached data with a long enough lifetime join the shell.
- Keep params reads inside Suspense so unknown URLs still get a shell.
- Shells that read cookies or headers are cached per session on the client.
- After a first visit, ISR upgrades the shell with the concrete params.
Frequently Asked Questions
Is an App Shell the same as the static shell?
Do I write a special file to get an App Shell?
Conclusion
An App Shell is the reusable part of a route that does not depend on URL data. Next.js 16 prerenders it so even an unlisted dynamic URL opens instantly, then fills in the specifics.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.