Guide · Frontend
Next.js Partial Prefetching Explained: How ensureStatic Keeps Routes Static
20 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Partial Prefetching in Next.js loads a route in three stages. First comes the App Shell, the part of a route that is the same for every link to it. Then an optional per-link prefetch, then the rest on click.
- Next.js 16.4.0, released on 6 October 2026, adds ensureStatic. It is a route setting that says which stages must come only from static output, meaning files built ahead of time and reused for everyone.
- Only ensureStatic = 'navigation' adds a build check: request-time work, such as reading a cookie, fails the build even inside a Suspense boundary, the wrapper that shows a loading placeholder. The 'shell' and 'prefetch' levels change what links fetch.
- Two new functions, prefetch() and navigation(), push one slow part of a page rendered at request time to a later stage. All of this needs the cacheComponents flag, and the stages also need the partialPrefetching flag.
- Beginners can stop after the section on how the three levels differ. Engineers then get a production test on 16.4.2: six prefetch links ran cookie code before any click. 'shell' did not stop it, while 'prefetch' and navigation() did.
What goes wrong when every product link prefetches on the server?
Each prefetched link can make your server render a page before anyone clicks. On a page full of links, that work can raise your hosting bill. A page that used to be static can also turn dynamic after one small edit.
This guide explains Partial Prefetching and the ensureStatic setting from Next.js 16.4. Together they decide what loads before a click, what waits for the click and what must never need the server.
Picture a common setup. An online spice shop runs on Next.js with product pages, a cart and free recipe pages. The setup is made up, but the problem is real. It follows the store example in the Next.js Keeping pages static guide; the prefetch counts later come from our own tests.
The team set prefetch to true on the 24 product cards on the home page. Product pages now open instantly. The hosting bill also rose from Rs 18,000 to Rs 31,000 a month (made up).
Each product page reads the shopper's cart cookie to show an In your cart label. So every visible card made the server render that product's cart status, even when nobody clicked.
The same week, someone added a Recently viewed strip to the recipe pages, and it reads a cookie. The recipe pages used to be fully static. Now part of each one renders on every request.
The team wants to fix both without making the site feel slower. The rest of this guide follows that fix.
Which words do you need before learning Partial Prefetching?
- Prefetching: downloading a page's code and data in the background before the user clicks, so the page can appear at once. Next.js does this only in a production build, not in next dev.
- Static output: HTML and data built ahead of time and reused for every visitor. It can be served from a CDN, a network of servers close to users, with no server rendering.
- Request-time rendering: the server builds the page fresh for one visitor, for example to read their cookies.
- Cache Components: the Next.js model switched on by the cacheComponents flag. You mark cacheable code with the 'use cache' directive, a line at the top of a file or function. Data fetching is dynamic by default.
- App Shell: the part of a route that does not depend on the link's URL, such as the menu. All links to one route share it. If the route reads cookies, the shell holds that visitor's data and the browser caches it per session.
- Route segment: one folder in the app directory, with its page or layout file. A route is made of segments, and a layout wraps every page below its folder.
- Suspense boundary: a React wrapper that shows a fallback, such as Loading reviews, until its content is ready.
What is Partial Prefetching and how does it work?
Partial Prefetching splits the loading of a route into three steps, which the docs call navigation stages. Each stage loads more of the page, and later stages can cost more server work.
- Stage 1, the App Shell: when a normal link scrolls into view, Next.js fetches one shared shell for that route. Twenty links to product pages share one shell.
- Stage 2, the per-link prefetch: a link with prefetch set to true also loads content for its own URL. An example is a product's details, found by its slug, the URL name like turmeric-500g.
- Stage 3, the navigation: when the user clicks, the server renders whatever is still missing.
Before Partial Prefetching, Next.js prefetched per visible link, so 24 links to 24 products meant about 24 prefetches. Now default links fetch one shell per route. The browser caches it, and every link to that route reuses it.
Think of a thali restaurant. The plate and the fixed sides are the same for every table, like the App Shell. A dish you pre-order while booking is like a per-link prefetch. Hot puris come only once you sit down, like the navigation.
Where the picture breaks: a kitchen cooks for each table, but a static shell is built once and handed to every visitor with no cooking at all.
The three navigation stages for one product link
Drag to exploreA default link stops at the shared shell. Prefetch set to true also loads the link's own content. Each later stage can need more server work.
Diagram connections:
- Product card scrolls in to App Shell fetched
- App Shell fetched to Prefetch set to true?
- Prefetch set to true? to Per-link prefetch (Yes)
- Prefetch set to true? to Shopper clicks (No)
- Per-link prefetch to Shopper clicks
- Shopper clicks to Navigation render
- Product card scrolls in: A shopper sees the link on the home page
- App Shell fetched: One per route (per session if it reads cookies)
- Prefetch set to true?: Chosen per link by the developer
- Per-link prefetch: Adds content for this URL, like product details
- Shopper clicks: The navigation starts
- Navigation render: Fills in everything still missing
On a product page, the menu and a Cart (0) placeholder can sit in the shell. The cached name, price and photo need the slug, so they can come with a per-link prefetch.
The shopper's own cart status needs their cookie. Whether it renders during a prefetch or waits for the click is exactly what the rest of this guide controls.
What does the ensureStatic route setting do in Next.js 16.4?
ensureStatic is a route setting that forces chosen navigation stages to come only from static output. You export it from a page or layout file, and it covers the whole route, including the layouts above that file.
It accepts five values: 'auto', 'shell', 'prefetch', 'navigation' and false. The default is 'auto', which adds no requirement. Each stricter level also covers the stages before it.
| Value | What must be static | Needs Partial Prefetching? | Adds a build check? |
|---|---|---|---|
| 'auto' (default) | Nothing extra | No | No |
| 'shell' | The App Shell | Yes. Without it, no effect and a warning | No |
| 'prefetch' | App Shell and per-link prefetch | Yes. Without it, no requirement | No |
| 'navigation' | The complete server-rendered route | No, Cache Components alone | Yes, in next dev and next build |
| false | Nothing; normal rendering | Documented for use with it | No, but a parent or child segment with a static level fails the build |
Levels combine across files. A level on a layout applies to every route that renders it. A child segment may set a stricter level, but a weaker one fails the build, and our 16.4.2 builds showed both.
How do the shell, prefetch and navigation levels of ensureStatic differ?
They differ in how far the static rule reaches: the shared shell, then per-link prefetches, then the full page.
The shell level keeps the shared shell static
'shell' requires the App Shell, which every default link loads, to be static. Personal content inside Suspense then waits for a per-link prefetch or the click.
The shop's store layout shows a cart count read from a cookie. In our 16.4.2 test, six default links rendered that count once on the server before any click. With ensureStatic = 'shell' on the layout, they rendered it zero times.
'shell' does not cover per-link prefetches. With prefetch set to true, the cart status still rendered on the server for every link.
The prefetch level keeps per-link prefetches static
'prefetch' requires both the App Shell and per-link prefetches to be static. Cached content tied to the URL still counts as static, so product details can still be prefetched. The cart status renders after the click.
This is the fix for the shop's bill. The 24 cards still prefetch the cached name, price and photo, but no card makes the server render a cart status any more.
// app/products/[slug]/page.js (built on Next.js 16.4.2)
// h is React's createElement, the call that JSX compiles to
import { Suspense, createElement as h } from 'react'
import { cookies } from 'next/headers'
export const ensureStatic = 'prefetch' // shell and per-link prefetch: static only
export function generateStaticParams() {
return [{ slug: 'turmeric-500g' }, { slug: 'jeera-200g' }] // built ahead of time
}
async function getProduct(slug) {
'use cache' // the same for every shopper, so it can be prebuilt
return { name: slug.split('-')[0], price: 120 } // stands in for a database query
}
async function Details({ params }) {
const { slug } = await params
const product = await getProduct(slug)
return h('h1', null, `${product.name}: Rs ${product.price}`)
}
async function CartStatus({ params }) {
const { slug } = await params
const cart = (await cookies()).get('cart')?.value?.split(',') ?? []
console.log('cart status rendered', slug) // count these lines in next start
return h('button', null, cart.includes(slug) ? 'In your cart' : 'Add to cart')
}
export default function ProductPage({ params }) {
return h('main', null,
h(Suspense, { fallback: 'Loading product' }, h(Details, { params })),
h(Suspense, { fallback: h('button', null, 'Add to cart') }, h(CartStatus, { params })),
)
}This page builds on 16.4.2. next build marks each product with a half-filled circle, which means static HTML with the cart status streamed in later. Change the level to 'navigation' and the same build fails, because the cookie is request-time data.
A rough cost model shows why the team cares. It counts server renders caused by prefetches alone, before anyone clicks.
home_views_per_day = 12_000 # made up
cards_with_prefetch = 24 # product cards with prefetch set to true
paise_per_render = 0.15 # server cost of one render, made up (100 paise = Rs 1)
def prefetch_renders(prefetch_is_static):
# A static prefetch comes from prebuilt output, so it needs no server render
per_view = 0 if prefetch_is_static else cards_with_prefetch
return home_views_per_day * per_view
before = prefetch_renders(prefetch_is_static=False)
after = prefetch_renders(prefetch_is_static=True)
saving = (before - after) * paise_per_render / 100 * 30
print(f"Server renders a day from prefetches: {before:,} -> {after:,}")
print(f"Rough saving: Rs {saving:,.0f} a month")The script prints 288,000 prefetch renders a day falling to 0, and a rough saving of Rs 12,960 a month (made up). That is close to the jump in the shop's bill. It assumes every listed product was built ahead of time.
The navigation level makes the whole route static
'navigation' requires the complete server-rendered route to be built ahead of time, for first page loads and for clicks. It needs only Cache Components, not Partial Prefetching. The team sets it on the recipe pages.
Choosing an ensureStatic level for a route
Drag to exploreStart with the strictest question and stop at the first Yes. Only the top answer adds a build check.
Diagram connections:
- Whole route static? to Use 'navigation' (Yes)
- Whole route static? to Prefetches static? (No)
- Prefetches static? to Use 'prefetch' (Yes)
- Prefetches static? to Shared shell static? (No)
- Shared shell static? to Use 'shell' (Yes)
- Shared shell static? to Leave it as 'auto' (No)
- Whole route static?: No request-time rendering allowed
- Use 'navigation': Build fails on any request-time work
- Prefetches static?: Many links into it use prefetch set to true
- Use 'prefetch': Shell and per-link prefetch from static output
- Shared shell static?: A layout reads cookies, like a cart count
- Use 'shell': Default links fetch only static output
- Leave it as 'auto': The default: no extra static rule
Beginners can stop here. You now know the three stages, the three static levels and which one fits each kind of page. The rest is for engineers who will set these up and check them.
Which ensureStatic settings actually fail the build?
Only 'navigation' adds static-output validation, and it runs in both next dev and next build. The docs say 'shell' and 'prefetch' add none, and our builds agreed.
With 'navigation', each of these failed our 16.4.2 build, even inside a Suspense boundary:
- Uncached data, such as a database call without 'use cache'. Calling connection(), a function that waits for a real request, fails too.
- Runtime data, such as cookies(), headers() and server-side searchParams, the URL's query string read on the server.
- A 'use cache' scope with a short cache life. That means expire under five minutes, stale under 30 seconds, or revalidate set to 0.
- A promise for request-dependent server data passed to a Client Component, even if the component never reads it.
These three settings come from cacheLife, the function that sets how long a cached value lives. stale is how long the browser may reuse it, and revalidate is how often the server refreshes it. expire is the longest a stale value may be served before Next.js must render it fresh.
We tested the edges. A stale of exactly 30 seconds and an expire of exactly five minutes both built. Of the built-in cacheLife profiles, the docs say only 'seconds' falls under these limits; it failed and 'minutes' built.
The docs add that the same rules apply to generateMetadata and generateViewport, the functions that build a page's title, meta tags and viewport settings.
A dynamic route, such as one URL pattern for all recipes, must also export generateStaticParams() with at least one set of values. Next.js builds those pages ahead of time. For a recipe not on that list, the first request waits while Next.js builds and stores the static page.
On the recipe pages, next build first asked for generateStaticParams(). Then it reported uncached or runtime data without naming the line. In next dev, the error pointed first to the uncached recipe query with its exact source line. Once that was cached, it pointed to the cookie read.
The docs suggest next build --debug-prerender when you can only run a build and need fuller stack traces. The team caches the query and moves Recently viewed into the browser. The build passes, and the listed recipe page now shows a hollow circle, meaning fully static.
How can you check that prefetches no longer hit your server?
Count server renders in a production build. Next.js prefetches only in production, for example after next build and next start, so a next dev test shows nothing.
Log the personal part
Add a console.log line inside the component that reads the cookie, like the cart status line in the sample above. Log fixed text and the slug only, with no timestamp, so runs stay comparable.
Build and start
Run next build, then next start, and open the page with the product links. In the browser console, run document.cookie = 'cart=jeera-200g' to set a cart cookie, then reload.
Wait without clicking
Leave the links on screen for about six seconds. Then count the log lines in the terminal running next start.
Change one thing and repeat
Add ensureStatic or navigation(), rebuild and count again. Zero lines before the click means the prefetches came from static output.
Pause and predict before reading the table. With ensureStatic = 'shell' on the product page and six links set to prefetch, how many cart status lines appear before any click?
| Product page setup | Links | Lines before any click | Lines after one click |
|---|---|---|---|
| No ensureStatic | prefetch set to true | 12 | 12 (no new line) |
| ensureStatic = 'shell' | prefetch set to true | 12 | 12 (no new line) |
| ensureStatic = 'prefetch' | prefetch set to true | 0 | 2 |
| await navigation() above the cookie read | prefetch set to true | 0 | 2 |
| No ensureStatic | Default links | 0 | 2 |
Read the table as before and after. With prefetch set to true, every link ran the cookie code on the server, and 'shell' changed nothing. 'prefetch' and navigation() both moved that work to the click.
One click printed two lines, and six prefetches printed twelve, so each render appears to log twice. We found no documentation of this as of 19 October 2026. Compare counts between runs rather than reading them as exact render counts.
Notice the first row: the click added no new line. In this setup, the per-link prefetch had already rendered this shopper's cart status, so the click appears to have reused it. Treat that as an observation, not a rule. When we added Date.now() to the log line, only four of six links rendered before the click, and the click rendered again.
How do the prefetch() and navigation() functions defer work?
They push one part of a page to a later stage, while ensureStatic sets a rule for the whole route. Both come from next/cache, and both throw an error without Cache Components.
- await prefetch(): during a request-time render, code below it stays out of the App Shell and runs during a per-link prefetch or the click. Without Partial Prefetching there is no shell, so it does nothing.
- await navigation(): during a request-time render, code below it stays out of the shell and per-link prefetches, so it runs after the click. Without Partial Prefetching, it still defers for default links, but a link with prefetch set to true fetches the whole route.
- Both apply only to request-time rendering. On a page built ahead of time, the build already ran that code, so a static prefetch can still carry its output.
- Neither changes the first load of a page. They only shape what happens during client navigations.
Place the call above the slow code and wrap that component in Suspense, so earlier stages show the fallback. The docs say calling either one inside a 'use cache' function throws. Our build with navigation() there stopped with an error saying so.
Unlike connection(), these functions do not make the code request-dependent. Data below them can still use 'use cache' and keep its cache lifetime. A route set to 'navigation' with navigation() above cached data built as fully static.
One trap: a page that exports the prefetch route setting, such as prefetch = 'partial', and also calls prefetch() has two things named prefetch. The docs say to import the function under another name, such as prefetchStage.
The shop's product pages show 40 reviews each (made up), from a cached query. The team tried await navigation() above that query. In our 16.4.2 test, the per-link prefetches still carried the reviews before any click.
That is expected. The product pages are built ahead of time, so the build already ran the reviews query, and static prefetches reuse that output with no server render. navigation() saves server work only for parts that would otherwise render at request time, like the cart status in the table above.
How do you turn on Cache Components and Partial Prefetching in your config file?
Set cacheComponents and partialPrefetching to true in next.config.js. partialPrefetching requires cacheComponents, and our build stopped with an error when only partialPrefetching was set.
// next.config.js (Next.js 16.4.2)
module.exports = {
cacheComponents: true, // the 'use cache' model; required by everything below
partialPrefetching: true, // App Shell per route; needs cacheComponents
}Leave partialPrefetching unset with Cache Components on and it behaves like false. Our build printed a warning asking us to set it to true or false.
cacheComponents became a stable top-level option in Next.js 16.0.0, and partialPrefetching arrived in 16.3.0. Neither sits under experimental. In 16.4.0, prefetch() and navigation() replaced the unstable_ names used in earlier canary (test) builds.
New projects get both flags. Running create-next-app 16.4.2 with its defaults wrote cacheComponents: true and partialPrefetching: true into next.config.ts in our test. The 16.4 post recommends Cache Components for every Next.js app and says it becomes the default in Next.js 17.
How do you add ensureStatic to an existing Next.js app?
Turn on the flags, read your build output, then add one level at a time.
Turn on both flags
Set cacheComponents and partialPrefetching to true. An app that cannot switch all at once can adopt Partial Prefetching route by route with prefetch = 'partial'.
Read the route list
Run next build. A hollow circle marks a fully static route. A half-filled circle marks static HTML with dynamic parts streamed in.
Pick a level per area
Use 'shell' for layouts with personal bits, 'prefetch' for pages behind many prefetched links and 'navigation' for fully public pages.
Fix what 'navigation' reports
Cache shared data with 'use cache'. Remove request data you do not need, or read it in the browser. Use next dev to see the exact source line.
Count prefetch renders
Repeat the log-counting check from earlier on your busiest page of links, before and after the change.
Not every route needs this. The 16.4 post says many dynamic apps won't need ensureStatic, and suggests it for ecommerce stores, marketing pages and blogs.
How do you explain Partial Prefetching in a system design interview?
Frame it as a cost and speed trade-off across three stages, then show where you would draw the static line.
Name the three stages
A shared App Shell per route, an optional per-link prefetch and the navigation render on click.
Explain the cost of each
Shell cost grows with routes, per-link cost with visible links and navigation cost with clicks.
Place the slow parts
Use prefetch() or navigation() to push expensive request-time work to a later stage, behind a Suspense fallback.
Guard the whole route
Add ensureStatic. Say clearly that only 'navigation' adds a build check, while 'shell' and 'prefetch' change what links fetch.
Use the running example
Mention 24 prefetched cards, a cart cookie and how 'prefetch' moved the cart status to the click.
Questions people ask
- Does ensureStatic = 'shell' fail the build if a page reads cookies?
- Not if the read sits inside a Suspense boundary. The docs say 'shell' and 'prefetch' add no build validation, and our 16.4.2 build passed. A cookie read outside Suspense does fail, but that is the normal Cache Components rule, not ensureStatic. Only 'navigation' fails the build on request-time work inside Suspense.
- Do I need Partial Prefetching to use ensureStatic?
- Only for 'shell' and 'prefetch'. Without Partial Prefetching there is no App Shell. So 'shell' does nothing and logs a warning, while 'prefetch' adds no requirement and links keep their old full prefetch. The 'navigation' level works with Cache Components alone.
- What is the difference between navigation() and connection()?
- During a request-time render, navigation() delays work until the click, while connection() makes work wait for a real request. Code below connection() is request-dependent and is left out of prerendering. Code below navigation() stays reusable, so it can use 'use cache' and keep its cache lifetime.
- Is Partial Prefetching on by default in Next.js 16.4?
- Not for existing apps: you opt in with the partialPrefetching flag, and leaving it unset behaves like false. New apps differ. create-next-app 16.4.2 with its defaults writes both cacheComponents and partialPrefetching as true. The docs say both will be on everywhere in the next major release.
- Does a link with prefetch set to true cost server time?
- It can. In our 16.4.2 test, six such links ran a cookie-reading component on the server before any click. If everything the prefetch needs is static or cached, Next.js serves it from static output instead. ensureStatic = 'prefetch' turns that into a rule for the route.
- Can a child page loosen a layout's ensureStatic level?
- No. A child route segment can set a stricter level, such as 'navigation' under a 'prefetch' layout. A weaker level fails the build. Leaving the export unset, or setting 'auto', keeps the parent's level. We checked all three cases on 16.4.2.
- Why does nothing change when I test prefetching in next dev?
- Because Next.js prefetches only in production. The prefetching guide says automatic prefetching runs only in production, so run next build and then next start to see prefetch requests or count server renders. next dev still shows the 'navigation' errors, with exact source lines in its overlay.
Sources and further reading
- Next.js 16.4 release post (Next.js blog, 6 October 2026)
- Next.js 16.3 Instant Navigations post, Rethinking prefetching section (Next.js blog, 16.3 Preview, 25 June 2026)
- ensureStatic route segment config (Next.js docs)
- Keeping pages static guide (Next.js docs)
- navigation function reference (Next.js docs)
- prefetch function reference (Next.js docs)
- partialPrefetching config option (Next.js docs)
- Prefetching guide, production-only prefetching (Next.js docs)
- Next.js security update, 14 October 2026: use cache fix in 16.4.1 (Next.js blog)
- npm registry record for the next package (release dates, latest tag)