---
title: "Next.js prefetch, measured: Partial Prefetching in 16.3"
description: "Partial Prefetching in Next.js 16.3 cut prefetched bytes by up to 93% in our benchmark, but URL content now lands after the click. When each setup wins."
canonical_url: "https://www.outofplace.space/blog/nextjs-prefetch-benchmark"
language: "en"
last_updated: "2026-09-23"
alternates:
  pl: "https://www.outofplace.space/pl/blog/nextjs-prefetch-benchmark.md"
---

# Next.js prefetch, measured: Partial Prefetching in 16.3

> Partial Prefetching in Next.js 16.3 cut prefetched bytes by up to 93% in our benchmark, but URL content now lands after the click. When each setup wins.

Published 2026-09-23 · Category: Performance · Tags: Next.js, Prefetching, React

[Next.js 16.3](https://nextjs.org/blog/next-16-3) changes what Next.js prefetch means. With [`partialPrefetching`](https://nextjs.org/docs/app/api-reference/config/next-config-js/partialPrefetching) on, the [App Router](https://nextjs.org/docs/app) no longer fetches a payload for every link on screen: it fetches one reusable shell per route and leaves the URL-specific content for after the click. We built one app six ways and clicked through it 3,744 times to see what that trade buys and what it costs.

On a docs page with a 100-link sidebar, prefetched bytes fell by 93%. The price is time: the content that used to be on screen almost at once now arrives a few hundred milliseconds after the click, behind a skeleton. Which side of that trade you want depends on the page, and on [one detail of React that is easy to miss](#the-skeleton-you-will-see-300-ms).

- Partial Prefetching cut prefetched bytes by 78–93% on link-heavy pages, and a whole docs navigation by about eleven times.
- The shell paints instantly. The URL content arrives after the click: 345ms on docs against 52ms with Cache Components alone, on a throttled phone.
- A skeleton that appears stays for about 300 ms, because React holds each reveal for that long.
- Links with `prefetch={true}` stay instant in every setup.
- Keep `prefetchInlining` at its default; HTTP/2 changed nothing that matters.

## What Partial Prefetching changes

Until 16.3, the App Router prefetched per link: as N links scrolled into view, it fetched about N route payloads. With `partialPrefetching` (it requires [`cacheComponents`](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents)), it fetches one [App Shell](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) per route: everything the page renders that doesn't depend on the URL. Links to the same route share that one prefetch. Content that reads `params` or `searchParams` resolves after the click, unless a link asks for more with `prefetch={true}`.

We compared six builds of the same app:

- **L0**, no Cache Components: the classic App Router.
- **C0**, Cache Components alone.
- **P0**, Cache Components with Partial Prefetching and the default prefetch inlining.
- **P1–P3**, the same with inlining off, with small thresholds and with large ones.

Each was clicked through three navigations: a catalog of 48 products to a product page, a docs page with a 100-link sidebar to another docs page, and a search results page to another search through one of eight `prefetch={true}` links.

## Prefetch bytes: up to 93% fewer

On the docs page, Cache Components alone prefetched every visible sidebar link. Partial Prefetching took that from 402 kB → 29 kB (−93%), in 8.5 requests instead of 49. On the catalog the saving was 78%. Search barely moved, because its eight `prefetch={true}` links still fetch their own results.

**Partial Prefetching prefetches a fraction of the bytes on link-heavy pages** (Compressed RSC bytes prefetched before the click · desktop, unthrottled, warm · medians of 20 runs)

| Item | Catalog → product | Docs → docs | Search → search |
| --- | --- | --- | --- |
| L0 · Classic router | 30 kB | 408 kB | 98 kB |
| C0 · Cache Components | 160 kB | 402 kB | 163 kB |
| P0 · Partial Prefetching | 34 kB | 29 kB | 107 kB |
| P1 · Inlining off | 33 kB | 32 kB | 105 kB |
| P2 · Small inlining | 32 kB | 28 kB | 103 kB |
| P3 · Large inlining | 34 kB | 33 kB | 105 kB |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-partial-prefetching-prefetches-a-fraction-of-the-bytes-on-link-h.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/summary_by_variant.csv

One row deserves a second look. Turning on Cache Components without Partial Prefetching made the catalog prefetch five times more than the classic router, 30 kB → 160 kB (+426%): its prerendered product shells become prefetchable per link. If you adopt Cache Components on link-heavy pages, decide on Partial Prefetching at the same time.

## Is navigation actually instant?

The shell is. Under Partial Prefetching it painted 43ms–45ms after the click on a throttled phone. The part people came for, the product or the article, is another story: it needs a request after the click.

**Cache Components alone shows the content at once; Partial Prefetching after a round trip** (Click to the first frame with the URL-specific content · mobile, Lighthouse-mobile throttling, warm click · ms)

| Item | L0 · Classic router | C0 · Cache Components | P0 · Partial Prefetching |
| --- | --- | --- | --- |
| Catalog → product | 375ms | 60ms | 262ms |
| Docs → docs | 47ms | 52ms | 345ms |
| Search → search | 40ms | 41ms | 43ms |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-cache-components-alone-shows-the-content-at-once-partial-prefetc.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/summary_by_variant.csv

On docs, Cache Components alone painted the new article in 52ms; Partial Prefetching took 345ms. On the product page the gap was smaller, 60ms → 262ms (+337%), and the classic router was slowest of all, because it had nothing prerendered for the dynamic route. Search, where every link carries `prefetch={true}`, painted in 40ms–43ms in every setup.

### The skeleton you will see: 300 ms

On the product page the three stages land one after another: the shell, the product, then the part rendered at request time.

**Under Partial Prefetching the page arrives in three steps** (Catalog → product · mobile, throttled, warm · click to each stage on screen · ms)

| Item | Shell | Content | Request-time part |
| --- | --- | --- | --- |
| L0 · Classic router | 375ms | 375ms | 375ms |
| C0 · Cache Components | 60ms | 60ms | 342ms |
| P0 · Partial Prefetching | 47ms | 262ms | 562ms |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-under-partial-prefetching-the-page-arrives-in-three-steps.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/summary_by_variant.csv

On docs, the skeleton stayed up 298ms in every Partial Prefetching build. That is not network time: the response had arrived after 250ms.

The React build inside Next.js 16.3.6 (`19.3.0-canary`) [waits until 300 ms](https://github.com/react/react/blob/d083ec1da1e5252abd3ddfdde6dfbc09701a2c51/packages/react-reconciler/src/ReactFiberWorkLoop.js#L528) after the last switch between a [Suspense](https://react.dev/reference/react/Suspense) fallback and its content before it commits the next one, so a loading state doesn't flash. The side effect: stages chain. A skeleton that appears is on screen for about 300 ms even when the data is already there, and a separate boundary for request-time data reveals 300 ms after the one before it. Design skeletons to be looked at, and consider one boundary where two would only chain.

### An early click

Clicking 50 ms after the page hydrates, before any prefetch finishes, takes the advantage away from everyone who prefetches. Cache Components alone is as slow as Partial Prefetching then, because both show a fallback first and then wait out the 300 ms.

**Clicked early, no setup has prefetched yet** (Click 50 ms after hydration · mobile, throttled · click to URL content · ms)

| Item | L0 · Classic router | C0 · Cache Components | P0 · Partial Prefetching |
| --- | --- | --- | --- |
| Catalog → product | 367ms | 374ms | 389ms |
| Docs → docs | 262ms | 528ms | 529ms |
| Search → search | 234ms | 404ms | 339ms |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-clicked-early-no-setup-has-prefetched-yet.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/summary_by_variant.csv

The last stage, the part rendered at request time, splits these clicks in two. With Partial Prefetching, 6 of 12 early clicks on the product page had the whole page on screen after 375ms–400ms. The other 6 took 650ms–700ms, and no run landed in between. The gap is about the 300 ms React waits between two reveals: the last stage arrives with the product or one wait later.

**The last stage lands with the product or about 300 ms later** (Catalog → product, early click · Partial Prefetching · mobile, throttled, HTTP/2 run · click to the last stage on screen · ms)

| Range | runs |
| --- | --- |
| 375ms–400ms | 6 |
| 400ms–425ms | 0 |
| 425ms–450ms | 0 |
| 450ms–475ms | 0 |
| 475ms–500ms | 0 |
| 500ms–525ms | 0 |
| 525ms–550ms | 0 |
| 550ms–575ms | 0 |
| 575ms–600ms | 0 |
| 600ms–625ms | 0 |
| 625ms–650ms | 0 |
| 650ms–675ms | 3 |
| 675ms–700ms | 3 |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-the-last-stage-lands-with-the-product-or-about-300-ms-later.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/runs.csv

## The whole bill

Partial Prefetching moves bytes from before the click to after it, and on the product page it also doubles them: the navigation response carried two complete copies of the product's payload, 13 kB → 26 kB (+99%) compressed. Counting everything a navigation downloads, prefetch included, it still comes out far ahead on docs.

**A whole docs navigation costs about a tenth of the bytes** (All compressed RSC bytes of one docs navigation, prefetch and navigation · mobile, throttled, warm)

| Item | Value | n |
| --- | --- | --- |
| L0 · Classic router | 463 kB | 20 |
| C0 · Cache Components | 458 kB | 20 |
| P0 · Partial Prefetching | 40 kB | 20 |
| P1 · Inlining off | 43 kB | 20 |
| P2 · Small inlining | 39 kB | 20 |
| P3 · Large inlining | 44 kB | 20 |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-a-whole-docs-navigation-costs-about-a-tenth-of-the-bytes.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/summary_by_variant.csv

About 11 times fewer bytes on docs, 2.2 times fewer on the catalog. Hydration and input delay on the start page didn't change in any setup.

## When `prefetch={true}` is worth it

[` `](https://nextjs.org/docs/app/api-reference/components/link#prefetch) fetches the destination's URL data too, so its click is as instant as a full prefetch. It is paid per visible link: the eight search links cost 107 kB, more than the whole docs page under Partial Prefetching. Use it where the click is likely and the data cacheable: search suggestions, a "next" link, the first few results. On grids of cards, prefetch on intent (hover or touch) instead of marking every card.

## prefetchInlining: leave it on

[`experimental.prefetchInlining`](https://nextjs.org/docs/app/api-reference/config/next-config-js/prefetchInlining) bundles small prefetch responses into one. The default and our two threshold settings differed by at most one request and a few kilobytes, with no measurable difference in time.

Turning inlining off added five or six requests on the catalog and search, and over HTTP/1.1 that cost an early click on the catalog: six connections per host fill up. Over [HTTP/2](https://www.rfc-editor.org/rfc/rfc9113.html) the penalty disappeared. Every other setup was the same on both protocols.

**HTTP/2 only matters with inlining turned off** (Catalog → product, early click · mobile, throttled · click to content · HTTP/1.1 and HTTP/2 interleaved, 12 runs each · ms)

| Item | HTTP/1.1 | HTTP/2 |
| --- | --- | --- |
| L0 · Classic router | 365ms | 363ms |
| C0 · Cache Components | 375ms | 373ms |
| P0 · Partial Prefetching | 387ms | 386ms |
| P1 · Inlining off | 440ms | 372ms |
| P2 · Small inlining | 390ms | 383ms |
| P3 · Large inlining | 387ms | 386ms |

Source: Outofplace benchmark, Next.js 16.3.6. Chart: https://www.outofplace.space/data/blog/nextjs-prefetch-benchmark/charts/en/chart-http-2-only-matters-with-inlining-turned-off.png Data: https://www.outofplace.space/data/blog/nextjs-partial-prefetching-benchmark/2026-09-23/summary_by_variant.csv

## Which setup to choose

```ts title="next.config.ts"
const nextConfig: NextConfig = {
  cacheComponents: true,
  // One App Shell per route instead of one prefetch per link.
  partialPrefetching: true,
};
```

```tsx title="The links where a wait would hurt"

  {suggestion}

```

Pages with many links to the same dynamic route (docs, grids, feeds) gain the most. A page with a handful of links to different pages gains little.

Where instant content matters more than bytes and the pages are static, keep a route on full prefetch; elsewhere turn Partial Prefetching on. The [segment config `export const prefetch = 'partial'`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/prefetch) adopts it one route at a time.

Add `prefetch={true}` where the next click is predictable and the data cacheable.

It will be seen for about 300 ms on a slow connection. Put the title and navigation in the part that doesn't depend on the URL: it paints almost at once.

This site runs Partial Prefetching: most of our pages have few links, and the ones that have many (the blog, the case studies) are static and small. Our own products, Portivo, Sprawna Matura and Levera, are built on Next.js too.

## How we measured

A generated App Router app (a 48-product catalog, 100 docs pages with a sidebar in the layout, a search page) was built six times with Next.js 16.3.6, differing only in `cacheComponents`, `partialPrefetching` and `experimental.prefetchInlining`, and served with `next start`. Headless Chrome opened a start page in a fresh browser context, waited, and clicked a link, recording every request over the [DevTools protocol](https://chromedevtools.github.io/devtools-protocol/) and timing marker elements from the click event to the next animation frame. Each combination of setup, scenario, phone or desktop, fast or [throttled network (150 ms, 1.6 Mbps, 4× CPU)](https://github.com/GoogleChrome/lighthouse/blob/main/docs/throttling.md) and early or late click ran 20 times in shuffled order, 2,880 runs; an HTTP/2 arm added 864 more. Medians carry seeded bootstrap 95% intervals.

It is a lab: one machine, localhost, no CDN, no images or fonts, generated text, and a synthetic 300 ms request-time delay. Throttling is per request, not packet-level, so absolute times are optimistic; the comparisons are what count. A late click is the best case for prefetching and an early one a specific race; real users click everywhere in between. The App Shell mechanics, the doubled product payload and the 300 ms reveal are specific to Next.js 16.3.6 and its React canary and may change in a patch release.

The shared shell is fetched through the URL of whichever link the router schedules first, and that response carries the whole page for that URL. A click on exactly that link needs no request at all. We picked click targets further down the page so that this didn't flatter Partial Prefetching.

## Further reading

- [Next.js: Adopting Partial Prefetching](https://nextjs.org/docs/app/guides/adopting-partial-prefetching): How the App Shell works and how to move routes over one at a time.

- [Next.js: Optimizing prefetching](https://nextjs.org/docs/app/guides/optimizing-prefetching): Per-link prefetching with prefetch={true}, and when to use it.

- [Next.js: prefetchInlining](https://nextjs.org/docs/app/api-reference/config/next-config-js/prefetchInlining): What inlining bundles, and why the default is right for most apps.
