Next.js 16.3 Instant Navigations: How Partial Prefetching Works
Next.js 16.3 introduced Instant Navigations, an opt-in App Router navigation model built around Cache Components, Partial Prefetching, and React’s <Suspense>. Instead of making every client navigation wait for the complete Server Component response, Next.js can have an App Shell ready in the browser and render it as soon as you click a link, while the remaining server-rendered content streams into its Suspense boundaries.
That distinction matters. “Instant” does not mean that every byte of the destination page has already loaded. It means the browser has enough static, cached, or fallback UI to start rendering the destination immediately. The current Next.js documentation also notes that this assumes warm caches; a cold cache can still require the server to compute a cached result once.
What Are Instant Navigations in Next.js 16.3?
In a traditional server-driven navigation, you click a link, the browser waits for a network round trip, the server renders the destination, and only then does the new UI appear.
Next.js 16.3 changes that model. With Instant Navigations, you decide which parts of a route can be available before the click and which parts are allowed to arrive afterward.
The framework gives you three main choices for asynchronous work: Stream it behind a Suspense boundary, Cache it with 'use cache', or deliberately allow the route to Block by opting it out of instant-navigation validation.
There is one subtle point that is easy to miss. A full page load and a client-side navigation do not necessarily have the same initial UI. If you navigate from /store/shoes to /store/hats, the shared /store layout is already mounted. Next.js only needs to render below that shared layout, so a Suspense boundary above it cannot provide the loading state for that transition. Your useful Suspense boundaries therefore need to exist below the shared layout where the navigation actually re-renders.
Why Can Next.js App Router Navigation Feel Slow?
Consider a product page that waits for the route parameter and then fetches its product before returning any UI:
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const product = await getProduct(slug)
return <ProductDetails product={product} />
}
If that asynchronous work is not represented by cached UI or by a Suspense boundary that is usable during the navigation, the destination has nothing useful to commit immediately. The user clicks and waits for server work.
A better structure is to move the runtime work into a child and expose an immediate shell around it:
import { Suspense } from 'react'
export default function ProductPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
return (
<main>
<ProductNavigation />
<Suspense fallback={<ProductSkeleton />}>
<ProductContent params={params} />
</Suspense>
</main>
)
}
async function ProductContent({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const product = await getProduct(slug)
return <ProductDetails product={product} />
}
Now ProductNavigation and the ProductSkeleton can participate in the destination shell, while ProductContent is allowed to resolve later. This is the same streaming model React documents for Suspense-enabled server rendering.
How Do You Enable Instant Navigations in Next.js?
Next.js 16.3 ships Instant Navigations as an opt-in model. Enable Cache Components and Partial Prefetching in next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
Install the current stable Next.js release rather than the old preview build:
npm install next@latest
cacheComponents enables the explicit caching model used by 'use cache'. partialPrefetching changes what App Router links prefetch: the default becomes a reusable App Shell for the route, while individual links can opt into more URL-specific prefetched content.
Next.js has stated that the behaviors behind Instant Navigations are intended to become defaults in a future major version, but in 16.3 you should still enable them explicitly.
How Do Stream, Cache, and Block Work?
How Do You Stream Dynamic Data with Suspense?
Use <Suspense> when the data should remain request-time or otherwise uncached, but the route should still have something useful to display immediately.
import { Suspense } from 'react'
export function ProductPage({ slug }: { slug: string }) {
return (
<>
<ProductSummary slug={slug} />
<Suspense fallback={<p>Checking inventory...</p>}>
<Inventory slug={slug} />
</Suspense>
</>
)
}
async function Inventory({ slug }: { slug: string }) {
const inventory = await getLiveInventory(slug)
return <p>{inventory.quantity} in stock</p>
}
The fallback is part of the UI Next.js can prepare ahead of time. The live inventory is still fetched on the server and streams into that boundary when ready.
How Do You Cache UI with 'use cache'?
Use the 'use cache' directive when the result is reusable between requests and does not need to be recomputed for every navigation.
async function ProductSummary({ slug }: { slug: string }) {
'use cache'
const product = await db.product.findUnique({
where: { slug },
})
return (
<header>
<h1>{product?.name}</h1>
<p>{product?.price}</p>
</header>
)
}
With Cache Components, a cached function receives a cache key derived from its build/function identity and serializable inputs. For a component such as this one, slug becomes part of that cache identity. Cached output can then participate in the static or prefetched UI instead of forcing the navigation to wait for the same work repeatedly.
How Do You Allow a Navigation to Block?
Sometimes you intentionally do not want to restructure a route for instant navigation. Next.js exposes the instant route-segment option:
export const instant = false
This opts the page or layout out of Instant Insights validation at that segment. The current documentation recommends using it narrowly: a parent opt-out can affect navigation into that part of the tree, while sibling navigations below it can still be validated. A route may also remain instant if its existing structure already supports it, so instant = false should be treated as an explicit permission to block rather than a performance technique.
What Is Partial Prefetching in Next.js?
Before Next.js 16.3, prefetching could become surprisingly aggressive. A page containing many links could trigger separate prefetch work for those links even when they pointed to different instances of the same route shape.
Partial Prefetching changes the unit of reusable work. Next.js builds an App Shell per route and reuses that shell across links to the same route.
Imagine a sidebar containing twenty links:
/chat/1
/chat/2
/chat/3
...
/chat/20
All of those URLs resolve through the same /chat/[id] route. With Partial Prefetching, Next.js can prefetch the reusable /chat/[id] App Shell once instead of treating every visible chat link as a completely separate shell.
The App Shell contains the static UI and cached UI that does not depend on the individual link’s URL. Dynamic work can remain behind Suspense and stream after the click.
This is the important optimization: prefetch the reusable shape of the destination, not necessarily the entire destination for every URL.
What Does <Link prefetch={true}> Prefetch Now?
With Partial Prefetching enabled, a normal <Link> prefetches the route’s App Shell by default.
<Link href={`/products/${product.slug}`}>
{product.name}
</Link>
If one particular navigation benefits from having URL-specific cached content ready before the click, opt that link into per-link prefetching:
<Link href={`/products/${product.slug}`} prefetch>
{product.name}
</Link>
In the 16.3 model, prefetch={true} can resolve URL data such as params, searchParams, and the full URL before navigation, and it can include cached content that depends on those values.
It does not mean “download every uncached dynamic part of the page.” Next.js 16.3 deliberately moved away from that old all-or-nothing interpretation. Uncached runtime content still belongs behind Suspense and can stream after navigation.
How Does Partial Prefetching Work Behind the Scenes?
The public API looks small, but the client router now does quite a bit of work before you click. If you follow the current Next.js source, the flow looks like this:
- When a Next.js link mounts,
mountLinkInstance()inpackages/next/src/client/components/links.tscreates the link instance and registers prefetchable links for visibility tracking. - An
IntersectionObservereventually reachesonLinkVisibilityChanged(). Viewport prefetching is intentionally disabled in development; in production, a visible link is queued for prefetch work. - Next.js creates a cache key for the destination and calls
scheduleSegmentPrefetchTask(), an alias of the segment-cache scheduler’sschedulePrefetchTask()inpackages/next/src/client/components/segment-cache/scheduler.ts. schedulePrefetchTask()creates aPrefetchTask, binds it to the active segment-cache map, starts it in theRouteTreephase, pushes it into the scheduler’s heap, and lets a microtask process the queue. The scheduler can therefore prioritize and batch prefetch work instead of firing every request immediately.- The prefetched route and segment data are stored in the client segment cache. During navigation,
readSegmentCacheEntryForNavigation()inpackages/next/src/client/components/segment-cache/cache.tsfirst looks for a fulfilled fallback entry. The source explicitly prefers a completed fallback shell over blocking on a more-specific entry that is still pending. - Once the shell can commit, React renders the static/cached UI and the nearest Suspense fallbacks. Uncached Server Component output can then arrive through the normal React Server Component/streaming path and replace those fallbacks as it becomes ready.
- In tests,
instant()frompackages/next-playwright/src/index.tsacquires a navigation-testing lock so Playwright can assert the prefetched or static UI before the remaining dynamic content is released.
That cache lookup is the part that makes the model particularly interesting. Next.js is not merely “prefetching faster.” The router is intentionally willing to use a less-specific fulfilled shell immediately instead of waiting for a more-specific pending cache entry.
For Instant Navigations, the developer feedback mechanism is Instant Insights and the development validation overlay, which points out blocking work, missing Suspense boundaries, and uncached data that prevents the route from being instant.
You can inspect the implementation directly in the official Next.js repository: links.ts, segment-cache/scheduler.ts, segment-cache/cache.ts, and next-playwright/src/index.ts.
How Do You Find a Next.js Navigation That Is Not Instant?
With Cache Components enabled, Next.js validates route segments in development. The default Instant Insights validation can identify a route that blocks, show where a Suspense boundary is missing, and surface uncached data that reaches the user before the framework has an immediate shell available.
This is useful because “I have a Suspense boundary somewhere above the page” is not enough. Next.js validates different navigation shapes separately. A boundary that works for a hard page load may sit above the shared layout during a soft navigation and therefore be irrelevant to that transition.
The Navigation Inspector in Next.js DevTools lets you pause the route at its initial shell. That is a practical way to inspect exactly what users can see before the remaining server work resolves.
How Do You Test Instant Navigations with Playwright?
Next.js 16.3 ships an instant() helper through @next/playwright. Install it with Playwright:
pnpm add -D @next/playwright @playwright/test
Then write a test around the UI that must be available before dynamic data finishes:
import { test, expect } from '@playwright/test'
import { instant } from '@next/playwright'
test('product navigation exposes its shell immediately', async ({ page }) => {
await page.goto('/store/shoes')
await instant(page, async () => {
await page.click('a[href="/store/hats"]')
await page.waitForURL('**/store/hats')
await expect(page.getByRole('heading')).toContainText('Baseball Cap')
await expect(page.getByText('Checking inventory...')).toBeVisible()
})
await expect(page.getByText('12 in stock')).toBeVisible()
})
Inside the instant() scope, you assert the part of the destination that must be ready immediately. After that scope is released, you can assert the streamed content.
When these tests run against next dev, the testing API is available automatically. If you run them against a production build with next start, the official guide requires the testing API to be exposed explicitly:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
experimental: {
exposeTestingApiInProductionBuild: true,
},
}
export default nextConfig
That gives you a regression test for perceived navigation behavior, not only for the final DOM after every request has completed.
Is loading.tsx Still Useful with Instant Navigations?
Yes, but it is no longer your only practical way to make a route responsive.
A loading.tsx file still gives a route segment a loading UI. The newer model lets you place Suspense boundaries directly around the components that actually perform runtime work, while 'use cache' lets stable pieces of UI move into the shell instead of becoming loading UI at all.
For a large page, that usually gives you more control than one coarse page-level skeleton. A product title can be cached, inventory can stream, recommendations can have their own boundary, and the surrounding layout can remain immediately usable.
Where Should You Use Instant Navigations?
Instant Navigations are most useful where users move repeatedly between related App Router destinations: SaaS dashboards, inbox or chat routes, product listing-to-detail flows, admin tables, account settings, and other interfaces where the shared shell is stable but some destination data is dynamic.
The practical placement rule is simple. Put Suspense boundaries as close as possible to the runtime work that truly needs to wait. Mark reusable asynchronous work with 'use cache' when its freshness model allows it. Let the default <Link> prefetch the shared App Shell, and use prefetch={true} only where URL-specific cached content materially improves the next screen. If a route is intentionally server-bound, put instant = false at the narrowest page or layout that needs the opt-out.
For existing applications, migrate route by route rather than treating Instant Navigations as a global “performance switch.” Enable Cache Components first, inspect the shell, enable Partial Prefetching, then add instant() tests around the navigation paths that matter to users. If you are building or modernizing a production React application, Contrive Solutions’ MERN and Next.js development services cover the same React, Node.js, and Next.js stack.
- Founded
- 2014
- Years in business
- 12
- People
- 66
- Headquarters
- Lahore, Pakistan
- Projects delivered
- 250+
- AI systems in production
- 10

