Reading the bundle treemap
Read your production JavaScript bundle with the built-in Turbopack analyzer, using a fixed set of scan passes to attribute weight to the exact module and import that caused it.
Here is a situation you will eventually find yourself in. INP is creeping up in Speed Insights, the production build feels heavier with every release, and yet every pull request that landed this month looks clean: a feature here, a refactor there, nothing that obviously added 200KB to every page. Nobody on the team can point at the dependency responsible, because “the bundle got bigger” is a feeling, not a line you can put your finger on.
This lesson gives you the instrument that turns that feeling into an attribution. You’ll learn to read a treemap of your production bundle: four scan passes that find the bytes that shouldn’t be there, and a triage tree that maps each finding to a fix. In the lesson on the Core Web Vitals you saw that INP rides on client JavaScript weight, and the previous two lessons named two ways that weight leaks: oversized images and barrel imports. The treemap is how you see and measure those leaks. The barrel fix you wrote in the lesson on the barrel-export trap is also where this pays off, because the treemap is how you confirm the chunk actually shrank.
What a treemap shows you
Section titled “What a treemap shows you”Before reaching for the tool, understand the picture it draws, because reading the picture well matters far more than which button you click.
A bundle treemap is a set of nested rectangles where area is bytes. The whole production bundle is the canvas. It’s sliced into routes, each route into chunks, and each chunk into the individual modules that make it up. A module that fills a quarter of the picture is a quarter of your bytes. That single equation, area is weight, is the entire reading skill. Everything else is learning where to look.
Three rules turn that equation into a discipline. Each one is a place beginners reliably go wrong, so treat them as traps to avoid rather than trivia.
First, read the transfer size, not the raw size. What hurts the user is the compressed bytes that travel over the wire, not the unminified source on disk. A library can look enormous in raw form and shrink dramatically once gzip compresses it. When the tool offers more than one size view, read the one closest to what the browser downloads: the compressed or transfer figure.
Second, filter to the client. The analyzer shows two worlds. Client modules are the JavaScript the browser downloads, which is what drives INP. Server modules are the Node bundle that runs on the server. For a performance-vigilance pass aimed at interaction latency, the client side is the one that matters. The server view has its own use, since it tells you how heavy your serverless functions are, which affects cold starts, but that’s a secondary question. Filter to the client first.
Third, the framework runtime is the floor. The single biggest legitimate tile is React plus Next’s runtime. Every app pays it, so it is the baseline, not the optimization target. It’s tempting to see the largest rectangle, assume biggest equals problem, and spend an afternoon trying to shave a framework you can’t shave. The floor stays the floor; leave it alone.
Hold those three rules in mind while you look at a treemap for the first time. Here is a representative one, walked tile by tile.
The chunk loaded on every route is the most expensive weight, because every page pays it, so watch it for creep release over release. This treemap is illustrative: area is bytes, and your real one comes from next experimental-analyze.
You’ll meet this exact picture again in a moment, because the four scan passes each map to one of the tiles you just saw highlighted. Learn the picture here, then learn the passes against it next.
Running the analyzer
Section titled “Running the analyzer”The choice of tool comes before any install, and here it’s easy. Turbopack is the Next.js 16 builder, so the analyzer you want is the one that understands the real Turbopack build. That’s the built-in Turbopack analyzer: no plugin to add, no next.config.ts to touch. It’s one command.
# Build the production bundle and open an interactive treemappnpm next experimental-analyze
# Skip the interactive view and write a static report to diskpnpm next experimental-analyze --outputThe first command builds the production bundle and opens an interactive treemap in your browser. Note the word production. The analyzer reads a production build on purpose, because the dev bundle is intentionally unoptimized and unsplit, so reading it tells you nothing about what ships. This is the same reason the optimizePackageImports rewrite from the previous lesson only runs in production: never trust a dev bundle for size.
The second command, with --output, skips the interactive view and writes a static copy of the report to .next/diagnostics/analyze. That’s the form you can keep. Copy it aside so you have a baseline to compare against after a fix:
cp -r .next/diagnostics/analyze ./analyze-beforeNow run a fix, re-run the analyzer, and diff the two. That before-and-after copy is also the foundation of the CI-artifact pattern at the end of the lesson.
Inside the interactive view, four controls matter, because the scan passes use them: filter by route, filter by environment (client or server), filter by type (JS, CSS, JSON), and search by file. The one feature that turns guessing into diagnosis is this: click a module and the analyzer shows its size and its import chain, the exact sequence of imports that pulled it into the bundle. When a tile surprises you, that import chain answers “why is this here?” by tracing the byte cost back to the line that caused it.
One more thing to clear up, because it trips up people following older tutorials. Next.js 16 removed the per-route “First Load JS” table that next build used to print, because those numbers were inaccurate for Server Component apps. If a guide tells you to read that build table, the guide predates version 16, so don’t go looking for it and don’t think your setup is broken. Your smell test now comes from elsewhere: real-user INP in Speed Insights, and the total-JavaScript number Lighthouse reports (the subject of the next lesson, on Lighthouse as the pre-launch gate). The treemap is what you open after the smell test trips. It’s the diagnosis, not the alarm.
The four scan passes
Section titled “The four scan passes”A treemap rewards a fixed reading order. Rather than wandering it hoping a problem jumps out, you run four passes every time, in the same order. Each pass maps to one of the tiles you saw highlighted in the figure above: a thing to look at, what it means, and where it points you next.
-
Biggest tile: expected or surprise? Find the single largest module. If it’s the framework runtime, that’s the floor, so leave it. If it’s a library you didn’t think you shipped, or a known-heavy one that’s larger than it should be, such as a date library, a charting library, or an icon set pulled in whole, that’s your lead. Click it, read the import chain, and find the exact line that added it.
-
Per-route weight: which routes carry heavy client chunks? Filter by route. A route with a fat client chunk almost always has a heavy interactive component on its leaves: a chart, a rich text editor, a map. The fix takes one of two shapes: move the work to the server if it doesn’t actually need interactivity, or code-split it with
dynamic()if it sits below the fold or only appears after a click. (The mechanics ofdynamic()come later in the course; for now, just know it’s the move to reach for.) -
Duplicate dependency: the same library twice. Scan for one package name showing up as two separate tiles, often as two versions. That means two copies of the same library in your dependency tree, usually from a peer-dependency mismatch. It’s pure waste, because the browser downloads the same code twice. The fix isn’t in your code; it’s at the package manager, with
pnpm dedupe. -
Shared chunk: did the floor rise? Look at the chunk loaded on every route: the shared runtime plus anything imported by a root layout or a global provider. It should stay close to constant from release to release. If it grew, a heavy library landed on every page at once, typically because something got added to a top-level provider or
layout.tsx. This is the most expensive kind of bloat, because every single route pays for it.
Step back and notice the shape of those four. Passes 1 and 2 find route-local bloat, weight that lives on one page. Passes 3 and 4 find global, structural bloat, weight that lives everywhere. Global bloat is the worse of the two, precisely because every route pays it, so when a shared chunk grows it deserves your attention first.
Running the same four passes every time is what makes the pass-to-meaning mapping automatic. Drill it once:
Each observation came from one of the four scan passes. Sort each finding into the pass that surfaced it. Drag each item into the bucket it belongs to, then press Check.
/dashboard ships a 200KB rich-text editor chunk.date-fns shows up as two tiles under different versions.From tile to fix: the triage tree
Section titled “From tile to fix: the triage tree”A finding is only useful if you know what to do with it. The four passes give you four kinds of finding, and this is the order an experienced engineer triages them in. What matters isn’t any single leaf; it’s running the questions in the right sequence so the cheapest, highest-leverage fix surfaces first. Walk the tree against whatever surprising tile you just found.
The framework runtime is the baseline every app pays, so there’s no win here. Note it and move to the next tile.
An unexpected heavy dependency is the highest-value fix on the board. Click the tile, read the import chain to find the line that pulled it in, and either delete that usage or swap it for a lighter alternative.
Two copies are pure waste. Run pnpm dedupe, then audit the peer dependency that forced two versions so it doesn’t drift back.
This is the barrel-export trap from the previous lesson. Add the package to experimental.optimizePackageImports, then re-run the analyzer to confirm the tile shrank.
This is a heavy interactive component on one route. Render it on the server if it doesn’t need interactivity, or load it lazily with dynamic() if it’s below the fold or behind a click.
Something pulled this into the root layout or a global provider and made every route pay. Find what imported it and move that import down to where it’s actually used.
That barrel leaf is where the previous lesson pays off. You added optimizePackageImports for an icon library, and the treemap is how you prove it worked. Run experimental-analyze before the change, run it again after, and watch the tile collapse.
The icon-library tile you’d watch shrink after optimizePackageImports. The shape is real, but the exact bytes are illustrative; run experimental-analyze before and after on your own build to read the true numbers.
What the treemap can’t see
Section titled “What the treemap can’t see”An instrument is only safe to trust once you know its blind spots. The treemap has two, and both matter for the INP thread running through this chapter, because both can produce a slow page that looks innocent on the treemap.
The first blind spot is runtime cost. The treemap measures static bytes, how much JavaScript ships. It says nothing about what that JavaScript does once it runs. A small bundle can still produce terrible INP: a synchronous JSON.parse of a large payload, an O(n²) loop in a click handler, a client tree that re-renders far more than it should. None of that shows up as area on a treemap, because it isn’t bytes; it’s work. The instrument for runtime work is the Chrome DevTools Performance panel, not the analyzer. Remember that bundle size and INP are correlated, not identical: a tiny bundle does not guarantee a fast interaction.
The second blind spot is third-party scripts. A <script> loaded through next/script, such as analytics, a chat widget, or a tag manager, isn’t part of your bundle, so it never appears on the treemap at all. Yet heavy third parties routinely outweigh a team’s own code on the wire. The treemap will happily show you a lean, well-triaged bundle while a tag manager quietly downloads more JavaScript than everything you wrote. The Network panel is where you see those, and the mitigations (deferring them with a lazy loading strategy, gating them behind consent) live in other lessons. The point here is narrower: the analyzer will never warn you about a third-party script, so you have to go look.
The treemap is a map of one thing, not a final verdict. It answers “what static bytes did I ship, and why.” The Performance panel answers “what does my code do at runtime.” Speed Insights answers “what do real users actually experience.” Each instrument owns one question, so reach for the right one.
Vigilance, not a one-off
Section titled “Vigilance, not a one-off”The most common way teams misuse the treemap is to run it once, breathe a sigh of relief, and never run it again. Bundles drift release over release, a dependency bump here, a convenience import there, and that drift is exactly the regression this whole chapter exists to catch. So treat the bundle audit as a habit, not a launch task. Two cadences cover it.
The first is the per-dependency-change pull request. Any PR that adds a dependency, bumps one, or drops in a heavy interactive component gets an analyzer pass. The cheap, structural version needs no bot at all: run next experimental-analyze --output, attach the report to the PR (or upload it as a CI artifact), and have the reviewer eyeball the diff against main. A single line in your PR template, “ran the analyzer, here’s the diff,” covers the whole regression class. A formal GitHub Action for this exists, but the well-known one (hashicorp/nextjs-bundle-analysis) is built around Webpack and the old build table, so on a Turbopack project the honest recommendation is the --output artifact plus a reviewer diff. Wiring it into CI properly is a sibling to the Lighthouse gate in the next lesson and builds on the GitHub Actions primitives taught later in the course; for now, just know the shape.
The second is the pre-launch deep pass. Once, before you ship, walk the treemap on your most important routes: the marketing page, the dashboard, the primary task screen. Run all four scan passes, triage each finding, fix it, and re-run to confirm. That’s the bundle-axis counterpart to the broader pre-launch audit in the next lesson on Lighthouse as the pre-launch gate.
One sentence ties the chapter together: field data tells you that the bundle regressed, and the treemap tells you what did.