Next.js 16.3 Turbopack Chunking: Instant Navigations Are Useless If the Wrong Chunks Ship
Muhammad Tayyab

Instant Navigations don't help if the browser still downloads the wrong JavaScript. Here's what a Turbopack chunk actually is, the official nextjs.org numbers, the 16.3 knobs, and the order I'd roll this onto my Sanity + App Router portfolio.
Yesterday Next.js shipped 16.3 and a post on Next.js 16.3 Turbopack chunking. Instant Navigations got the headlines. Chunking is the quieter change: which JavaScript files actually leave the CDN. My portfolio is still on 16.2.1 — Sanity, App Router, a next.config.js that only lists Sanity image remotePatterns. Instant clicks don’t help if you download the wrong JS. Here’s the merge math, the official nextjs.org numbers, and how I’d roll the new knobs onto iamtayyab.com without inventing fake Lighthouse wins.
I'm Muhammad Tayyab, a full stack and mobile developer in Lahore. I ship this site from iamtayyab.com — source on GitHub.
I’ve already written about building this blog engine and fixing the Lighthouse score. This isn’t a rerun of either. This is the download gap: the JS files Turbopack decides to merge, or not, before the browser even talks to your server.
What a chunk actually is
Open DevTools on a Turbopack-built Next.js app, click a .js file, unminify it, and you’ll see something like (globalThis.TURBOPACK || (globalThis.TURBOPACK = [])).push([...]. That file is a chunk. It holds your code, a dependency, or a slice of the runtime that wires the rest together.
"Chunking" is the boring-sounding job of deciding which modules share a file.
Three naive strategies all fail, and the Next.js team walked through why.
One giant chunk for the whole app. Every page after the first is a cache hit. Navigation looks fast. Then home downloads JS for /studio and routes nobody landed on. As the site grows, every landing gets heavier.
One chunk per page. Slim, never over-ships. Then every page that uses the same footer embeds that footer again. Visit four pages, download the footer four times. Caching dies.
One chunk per module. Nothing is over-shipped, shared modules download once. Then you have hundreds of tiny requests. HTTP/2 made requests cheaper, not free. Gzip also hates a pile of tiny files — it only finds repeated patterns inside a single file.
So you want the lowest download size *and* the fewest requests, and those two goals fight.
Turbopack’s answer is chunk groups. A chunk group is the set of chunks a route loads together. / is one group. /blog is another. Turbopack only merges chunks that already live in the same group, so merging can’t add code that page wasn’t already going to download.
The hard part is *when* merging helps.
Take two chunks in the home group’s set: A (needed everywhere) and B (home only). Merge them.
If someone lands on home and bounces, merge always wins. One request instead of two, and both chunks were needed anyway.
If they then navigate to a page that only needs A, the merged A+B file is useless there. The browser downloads A again on its own. You’ve now paid for A twice.
Merging only pays off across a navigation when *both* pages need *both* chunks. Then the merged file is reused and you saved a request.
Turbopack weights that tradeoff. Default guess: about two-thirds of sessions are a single page, one-third involve a navigation. That’s why bounce-heavy sites want more aggressive merging, and click-around products want less.
Official numbers from nextjs.org
I don’t have a 16.3 production HAR on this portfolio yet — I’m still on 16.2.1 — so I’m not going to fake "I saved 40 KiB" numbers. These are the official nextjs.org numbers from their own navigation series.
They ran the same path three ways: never merge, Turbopack defaults, and one chunk per group (max merge).
Strategy · JS downloaded · Requests
No merging · 561.6 KiB · 96
Turbopack defaults · 554.8 KiB · 38
One chunk per group · 610.0 KiB · 15Defaults cut requests by more than half versus no merging, and shipped slightly *less* code. Max merge cut requests further but shipped 10% more code overall (610.0 vs 554.8). If they’d navigated less, max merge would have looked better. Initial load loves big merges. Long sessions don’t.
The Next.js 16.3 Turbopack chunking knobs
16.3 puts this behind experimental.turbopackChunking in next.config.js. Two limits of the old algorithm: merge decisions happen at build time (the bundler can’t see what’s already in the browser cache), and the 2/3 single-page weight is a guess.
generateComponentChunks attacks the first limit. Turbopack emits the un-merged pieces alongside the merged files. At request time the runtime picks whichever is cheaper: the merged chunk, or just the missing pieces. It works in reverse too — if you already have merged A+B, skip re-loading A alone. Soft navigations load less waste. You keep merge benefits without the navigation tax.
The other knobs let you stop guessing:
firstPageLoadPriority— 0 to 1, default0.67. Shifts weight toward a fast first load vs cheap later navigations. Bounce rate is a reasonable starting value.priorityRoutes— pages whose load speed matters most. Merge more opportunistically there.clusters— arrays of regexes for routes people visit together. Overlapping chunks merge more readily inside a cluster. Mix pages that need only A or only B with pages that need both, and it’ll merge *less*.
Two sibling flags are about shipping less JS in the first place, not grouping it:
experimental.turbopackCjsTreeShaking— tree-shake CJS the way ESM already could. Unused imports were shipping. On by default in a future version.experimental.turbopackSharedRuntime— one runtime chunk instead of per-page ones. Saves a blocking request and about 10KB of client JS on every navigation after the first. Also destined to be default.
The default runtime also got lighter: no WASM / worker code until something actually uses those modules.
Copy-paste starting point — keep your existing images.remotePatterns for Sanity, add this:
/** @type {import(‘next’).NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
// existing Sanity image hosts stay here
],
},
experimental: {
turbopackCjsTreeShaking: true,
turbopackSharedRuntime: true,
turbopackChunking: {
generateComponentChunks: true,
firstPageLoadPriority: 0.75, // opinion: bounce-heavy portfolio, see below
priorityRoutes: [‘/’, ‘/blog’],
clusters: [
[‘^/$’, ‘^/blog$’, ‘^/blog/’],
],
},
},
}
module.exports = nextConfigI would not dump /studio into that public cluster. Studio is a fully client-side React app. Clustering it with / and /blog tells Turbopack readers visit those together. They don’t. Keep Studio out so public pages don’t get Studio-shaped merge decisions.
How I’d roll this onto iamtayyab.com
I’m not flipping every experimental flag the night 16.3 lands. The site is a Sanity + App Router portfolio. Readers land on home, maybe hit /blog, maybe open a post, often leave. That’s a bounce-shaped graph with a short content path — not a product with twenty authenticated routes.
Here’s the order I’d actually use.
1. Upgrade. Install the latest next from 16.2.1 to 16.3. Don’t touch Instant Navigations yet. Default upgrade wins are already worth it: about 90% less RAM in next dev, a disk cache for next build, native Node streams that handle up to 22% more requests under load, and fewer prefetch requests. That’s free. No config.
2. Baseline a HAR. Home → /blog → a post. Chrome DevTools, Network, disable cache, record the three-step path, export. Count JS transferred and request count. That’s the before. I haven’t run this on 16.3 yet, so I won’t pretend I have a delta.
3. Cheap flags. Turn on turbopackCjsTreeShaking and turbopackSharedRuntime first. Tree-shaking unused CJS and sharing one runtime is hard to regret. Re-record the same HAR. If JS transferred or request count gets worse, revert and stop. If it’s flat or better, keep them.
experimental: {
turbopackCjsTreeShaking: true,
turbopackSharedRuntime: true,
}4. Then `generateComponentChunks`. This is the interesting one. It costs extra emitted files at build time so the runtime can pick merged vs pieces. On a content site with a real home → blog → post path, that’s exactly the A-then-only-A failure mode the algorithm was eating. Re-record the HAR. Same path, same machine, same "disable cache."
5. Tune weight and clusters last. Opinion, labeled as such: I’d set firstPageLoadPriority to 0.75 on this portfolio. It’s a content site. Most sessions are a landing plus maybe one more URL. 0.75 is a bit more first-load hungry than the 0.67 default, still not "merge everything." For a marketplace like WorkConnect — lots of list → detail → apply clicks in one session — I’d start closer to 0.4 and let navigations win. Bounce rate is the documented starting value; I’m using the *shape* of the product as a proxy until I have analytics that say otherwise.
priorityRoutes: [‘/’, ‘/blog’] matches how people enter this site. Cluster public content only. If a cluster mixes "needs only A" pages with "needs A+B" pages, Turbopack merges less — don’t regex the whole app into one cluster.
Instant Navigations vs chunking
These get mashed together because they shipped in the same week. They close different gaps.
Instant Navigations is the server gap. You click a link, then you wait for the server to render. Stream with Suspense, cache with ‘use cache’, or explicitly block. The client already has a shell, so the click feels like an SPA. It’s opt-in: cacheComponents plus partialPrefetching.
/** @type {import(‘next’).NextConfig} */
const nextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
module.exports = nextConfigTurbopack chunking is the download gap. Which JS files does the browser fetch, how many round trips, and do you re-download A because you merged it with B. Instant Navigations can make the *server* instant and still feel slow if the click pulls a fat, badly-merged bundle.
Instant Navigations are useless *for that click* if the wrong chunks ship. They’re complementary, not a substitute.
On this site I would take the 16.3 default upgrade immediately, take the cheap chunking flags after a HAR, and treat Instant Navigations as a second project. Cache Components changes the rendering model. A Sanity blog with ISR-style pages is not the same migration as a dashboard that’s already ‘use cache’-shaped. I want the download graph honest before I restack the server graph.
The 16.3 announcement is worth reading for the default wins even if you never flip Instant Navigations.
Sources
All three of these are dofollow on purpose — go read the primary sources, don’t trust a summary for merge math:
FAQ
What is a Turbopack chunk? A JavaScript file Turbopack emits as a TURBOPACK.push payload. Chunking is which modules share that file.
Do I need Instant Navigations to use the new chunking knobs? No. experimental.turbopackChunking is independent. Instant Navigations wants cacheComponents and partialPrefetching. Chunking wants the turbopack flags. You can upgrade to 16.3 and only take chunking.
Is `firstPageLoadPriority` the same as bounce rate? It’s the documented starting point, 0–1, default 0.67 (the 2/3 single-page prior). I’m using 0.75 as an *opinion* for this portfolio because it’s content-first. I’d use ~0.4 as an opinion for a clicky marketplace. Measure, don’t cargo-cult my numbers.
Why keep `/studio` out of the public cluster? Clusters tell Turbopack "these routes are visited together, merge overlapping chunks more readily." My readers do home → blog → post. They do not do home → Sanity Studio. Mixing those graphs is how you get merge decisions that help me and tax them.
Will this magically raise my Lighthouse score? Maybe, maybe not. Lighthouse is a lab first-load. Aggressive merging can look great there and worse on a real multi-page session — that’s literally what the 610.0 KiB max-merge row shows. Record a HAR of the path you care about.
If you want this on your stack
If you’re on App Router and staring at a 16.3 upgrade — get in touch. I’ll start with a HAR, not a vibe.
Muhammad Tayyab is a full stack and mobile developer in Lahore, Pakistan. He builds Talk Motion, WorkConnect, and Black Seal — available for React/Next.js, Node.js, and SwiftUI projects. Get in touch · GitHub · LinkedIn · X.