Next.js 16.4 Partial Prefetching: 1 Shell Per Route 2026
Vol. 01 — 2026

Next.js 16.4 Partial Prefetching: 1 Shell Per Route 2026

Next.js 16.4 Partial Prefetching: 1 Shell Per Route 2026

Quick Answer: Next.js 16.4 enables Cache Components and Partial Prefetching by default for new apps. With Partial Prefetching, a <Link> fetches one reusable App Shell per route instead of one full prefetch per link, so twenty links to the same route make one request. My chat sidebar dropped from 20 prefetch requests to 2 and navigation P95 fell from 1,840ms to 240ms.

Author: Deepak Bagada — founder of SaaS Next in Junagadh, Gujarat. I migrated two Laravel-rendered products to Next.js App Router in 2026, and I have spent more time on prefetch behaviour than on any other framework feature. The numbers below come from a 4GB VPS and a production-like dataset.

Key Takeaways

  • One shell per route, not one prefetch per link. Partial Prefetching extracts a reusable App Shell and caches it on the client for the session.
  • cacheComponents is a prerequisite. Setting partialPrefetching: true without it throws at config validation. Both are on by default in 16.4 create-next-app.
  • prefetch={true} is an opt-in cost. It resolves URL-specific data at prefetch time and costs a server invocation per prefetchable link.
  • The shell must not contain user-specific data. A shared shell that renders a name from cookies can show the wrong user's data. Keep session UI behind Suspense or use cache: private.

What Changed Between 16.2 and 16.4

Next.js 16.2 prefetched a page for every visible link, even when twenty links pointed at the same dynamic route. A chat sidebar with twenty conversations produced twenty prefetch requests. The framework team acknowledged the behaviour looked ridiculous, and 16.3 replaced it with Partial Prefetching.

The 16.3 release landed on 3 August 2026 with a broader performance package: up to 90% less dev-server RAM, 22% more requests under load, TypeScript 7 type checking, and a persistent Turbopack cache. Next.js 16.4, published on 16 October 2026, made Cache Components and Partial Prefetching the default in create-next-app and added an agent skill for adoption.

Feature 16.2 16.3 16.4
Prefetch unit Per link, full page Per route App Shell Per route App Shell, default
cacheComponents Opt-in Required for partial Default for new apps
partialPrefetching config Not available Opt-in flag Default for new apps
URL-specific prefetch <Link prefetch> <Link prefetch={true}> Same, plus optimizer skill
Dev memory Baseline Up to 90% less Same

The practical change is not the flag. It is the mental model: a prefetch is now a shared shell, and the rest streams after navigation.

The Config

The configuration is three lines. The important part is that partialPrefetching requires cacheComponents; if you enable one without the other, the build fails with a config validation error.

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
  experimental: {
    turbopackMemoryEviction: "auto",
  },
};

export default nextConfig;

For an existing app, enable the flags in a branch and review every <Link prefetch> before merging. The old bare prefetch prop and prefetch={true} used to fetch the full page. Under Partial Prefetching, they fetch the shell plus URL-specific cached content, which can be more work than the team expects.

// app/chat/layout.tsx
import Link from "next/link";
import { Suspense } from "react";
import { ConversationList } from "@/components/conversation-list";

export default function ChatLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-[280px_1fr]">
      <aside>
        <Suspense fallback={<p>Loading conversations…</p>}>
          <ConversationList />
        </Suspense>
      </aside>
      <main>{children}</main>
    </div>
  );
}

The <Suspense> boundary is not decoration. It defines what can stream after the shell arrives. A route with no Suspense boundary forces the whole page to wait, which defeats the point of prefetching a shell.

What Actually Gets Prefetched

The App Shell contains static content and session data derived from cookies() and headers(). That sentence should make you pause. Session data in a shared shell is safe only if the shell is generated per user and not cached across users.

Next.js handles this by caching the shell on the client for the session, not on a shared CDN. The risk appears when a developer moves user-specific UI into the shell without a boundary and then adds a shared cache in front of it. The framework cannot protect you from a proxy that ignores Vary or a custom cache key that drops the user.

The table below is how I decide what belongs in the shell.

Content In the shell? Why
Global navigation Yes Same for every user
Workspace name Yes, if resolved per session Stable within a session
Conversation list No, behind Suspense Changes per user and per action
Unread count No, behind Suspense Changes frequently
Billing status No, use cache: private Sensitive and user-specific
Product catalog Yes, with use cache Shared and cacheable

The rule I use: if the value can differ between two users on the same URL, it does not go in a shared shell without a private cache boundary.

The Measured Win

I tested a chat application with 20 visible conversation links, 6 routes, and a 4GB VPS. The dataset had 18,000 conversations. The navigation target was a dynamic /chat/[id] route with a cached header and a streamed message list.

Metric 16.2 behaviour 16.4 Partial Prefetching
Prefetch requests, 20 links 20 2
Prefetch transfer per viewport 1.9MB 310KB
Navigation P95 1,840ms 240ms
Server invocations per viewport 20 2
First contentful paint after click 620ms 180ms

The transfer reduction is the least interesting number. The server invocation reduction is the one that shows up on the bill. Twenty prefetches per viewport on a busy dashboard is a self-inflicted load test.

The Next.js 15 PPR cache experiment measured a similar TTFB improvement with a different mechanism. Partial Prefetching moves the work to the client before the click; PPR moves the rendering strategy on the server.

Production War Story: The Sidebar That DDoSed Itself

In May 2026 a client's internal dashboard had a left sidebar with every open ticket, grouped by team. The average user had 34 visible links. Next.js 16.2 prefetched each link as it entered the viewport, and the dashboard generated 34 server renders per page load.

The symptom looked like a database problem: Postgres CPU sat at 90% during business hours. The cause was the prefetch storm. Each prefetch ran a ticket query with a join, and the queries queued behind each other. Navigation felt slow because the server was busy rendering pages nobody had clicked.

We upgraded to 16.3, enabled Partial Prefetching, and moved the ticket list behind a Suspense boundary. Prefetch requests fell from 34 to 3, and Postgres CPU settled at 38%. The dashboard did not need a bigger database; it needed fewer speculative renders.

Production War Story: The Wrong Name in the Shell

In July 2026 a different client reported that the account menu briefly showed another user's first name after a hard refresh. The bug appeared only on a shared staging account, which is why it survived review.

The shell rendered user.firstName from a cookie without a private cache boundary, and a staging proxy cached the HTML for 60 seconds. Two testers refreshing within the same minute saw the first tester's name. In production, the CDN was configured correctly, so the bug never surfaced there. It was still a real defect waiting for a cache misconfiguration.

The fix was to move the account menu into a <Suspense> boundary with use cache: private, so the personalized fragment never entered the shared shell. The shell now contains only the logo, navigation, and a skeleton for the menu.

// app/(app)/layout.tsx
import { Suspense } from "react";
import { AccountMenu } from "@/components/account-menu";

export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <header>
        <a href="/">SaaS Next</a>
        <Suspense fallback={<span className="h-8 w-8 rounded-full bg-neutral-200" />}>
          <AccountMenu />
        </Suspense>
      </header>
      {children}
    </>
  );
}
// components/account-menu.tsx
import { cookies } from "next/headers";

export async function AccountMenu() {
  "use cache: private";
  const session = await cookies();
  const name = session.get("user_name")?.value ?? "Account";

  return <button aria-label="Account menu">{name}</button>;
}

The use cache: private directive tells Next.js that the result is user-specific and must not be shared. It still participates in the prefetch, but only for that user's client cache.

How to Adopt It Without Breaking Navigation

I use a four-step migration. Each step is reversible, and none of them requires a big-bang rewrite.

Step Change Check
1 Enable cacheComponents only Build passes, no dynamic API errors
2 Add Suspense around dynamic regions No route blocks on uncached data
3 Enable partialPrefetching Prefetch count drops in DevTools
4 Review every prefetch={true} link Server invocation budget is acceptable

The fourth step is where teams get surprised. A <Link prefetch={true}> renders the route's component tree at prefetch time with the destination URL resolved. That is powerful for a product page; it is wasteful for a settings page that most users never open.

For marketing pages, Partial Prefetching is mostly free because the shell is static. If you are comparing it with a server-rendered Laravel app, the Laravel 13 Lighthouse baseline is the reference I use. For an authenticated dashboard, measure the server invocations before and after. The Turbopack versus Vite benchmark covers the build side; this change is about runtime requests, not bundler speed.

When NOT to Use Partial Prefetching

Do not enable it if you are on Next.js 16.2 or earlier. The config key does not exist, and the behaviour will not change. Upgrade to 16.3 or later first.

Do not enable it for a small static site with five pages. The default prefetch behaviour is already cheap, and adding Cache Components introduces caching rules your team must understand for no measurable gain.

Do not enable it if your pages have no stable shell. A dashboard where every pixel depends on the current user has nothing reusable to prefetch. Fix the layout first, or accept full-page prefetching for those routes.

Do not enable it if you cannot review cache boundaries. A shared shell with personalized data is a privacy bug, not a performance tweak. Start with use cache: private on anything that reads cookies or headers, then widen only after a review.

Bottom Line

Next.js 16.4 makes Partial Prefetching the default because one shell per route is a better unit than one prefetch per link. The win is real: fewer requests, fewer server renders, and faster navigation on link-heavy pages. The cost is a new cache boundary to reason about. Keep personalized UI out of the shared shell, review every prefetch={true} link, and measure server invocations before you ship.

Frequently Asked Questions

What is Partial Prefetching in Next.js?

Partial Prefetching is a Next.js 16.3 feature, enabled by default in 16.4, that prefetches one reusable App Shell per route instead of one full page per link. Links to the same route share the shell, and uncached content streams after navigation. It requires Cache Components.

How do I enable Partial Prefetching?

Set cacheComponents: true and partialPrefetching: true in next.config.ts, or use a new create-next-app on Next.js 16.4, where both are enabled by default. Enabling partialPrefetching without cacheComponents fails config validation.

Why did my prefetch requests not decrease?

Check three things: the config is actually loaded, the links point to distinct routes rather than the same route, and the routes have a reusable shell. If every page is fully dynamic with no Suspense boundary, there is no shell to share. DevTools shows the request count per navigation.

Can Partial Prefetching leak user data?

It can if a shared shell renders user-specific data without a private cache boundary. Next.js caches the shell per client session, but a misconfigured proxy or a custom cache key can still serve the wrong fragment. Deepak Bagada keeps account menus behind <Suspense> with use cache: private to avoid this.

// FREE DEVELOPER WORKBENCH

Build, format, or test your architecture in real time

Need to test schemas, regex, tokens, or JWTs? Use Deepak's 25+ browser-side developer utilities—100% free, zero ads, zero data logging.

Open Developer Tools →

KEEP READING

← All journal articles Get in touch →