App Shells in Next.js: Prerendering the Static Frame

What an App Shell is in Next.js 16, what ends up inside it, and how it gives every dynamic route an instant first paint.

7 min read

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.

Which flags this needs

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 contentIncluded when
Static markupAlways
Cached datastale time is 5 minutes or more
Suspense fallbacksAlways

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.tsxApp.tsx
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Is an App Shell the same as the static shell?

Close, but an App Shell is the URL-independent version of the static shell used when route params are not known at build time.

Do I write a special file to get an App Shell?

No. You get it by keeping params reads inside Suspense boundaries, which lets Next.js prerender the reusable frame.

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.