After running `next build` on a Next.js app, the route table prints a "First Load JS" column per route plus a "First Load JS shared by all" line. What does each number actually count?
answer
- it is a cold-entry figure, not a per-visit one
- the rows are not disjoint
- one line moves every route at once
- Size is the route's own contribution
- JavaScript only — no CSS, no server code
basics
~20 sFirst Load JS is all the JavaScript a browser downloads to render that route on a cold visit: the route's own chunks plus the shared chunks every route needs. The "shared by all" line breaks out that common baseline, which is included in every route's figure.
solid answer
~50 sThe per-route First Load JS is the total client JavaScript needed to render that route when it is the first page of the session — the route's own chunks, the chunks of the layouts wrapping it, and the shared baseline. The "shared by all" line is that baseline on its own: the framework runtime and the modules common to every route. Two things trip people up. First, the numbers overlap — you cannot add the route column down the page to get a total, because every row already includes the shared portion. Second, it is a cold-visit figure; a user who client-navigates from another route only fetches what is new, since the shared chunks are already cached. The `Size` column beside it is the route's own contribution, which is the number that moves when you change one page. Treat First Load JS as the budget signal for landing on a page cold.
code
javascript · 10 linesimport bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default withBundleAnalyzer(nextConfig)go deeper
Know that First Load JS is the JavaScript a cold visit to that route downloads, and that the shared line is the common part every route includes. Know that a lower number means a lighter first paint.
Explain that rows overlap so they cannot be summed, that Size is the route's own contribution, and that only client JavaScript is counted — so moving work to the server shows up as a drop.
Show you use it as a regression signal: track the shared line across releases, know that a shared-line rise hits every entry point, and pair the table with the analyzer to name the module responsible rather than guessing.
Own the budget: pick thresholds that reflect your users' devices and networks, decide whether CI fails or merely reports on a regression, and make sure the team distinguishes shared-baseline growth from leaf-route growth when prioritising.
## Reading the table `next build` ends with a table of routes. For the App Router the columns are the route path, `Size`, and `First Load JS`, followed by a legend for the route markers and a separate `+ First Load JS shared by all` line with the shared chunks broken out beneath it. The mental model is a stack of layers a browser must have before that route can render in the client: 1. The **shared baseline** — the framework and React runtime, and modules imported from so many places that the bundler hoisted them into a common chunk. 2. The **layout chunks** for every layout wrapping that segment, including the root layout's client components. 3. The **route's own chunk** — the page's client components and everything they statically import. `First Load JS` is the sum of all three. `Size` is roughly the third layer alone: what this route contributes on top of what it inherits. ## The two misreadings **Adding the column down the page.** The rows are not disjoint. Every route includes the shared baseline, so summing them massively overcounts — it counts the shared chunks once per route. If you want "how much JavaScript does this app ship in total", the route table is the wrong instrument; the analyzer's treemap of the client bundle is the right one. **Reading it as what every visit costs.** It is a *first* load figure. Once the shared chunks are in the browser cache, a client-side navigation to another route fetches only what that route adds. So a fat shared baseline is worse than a fat leaf route: everyone pays it once and it sits on the critical path of every cold entry point, while a heavy leaf route is only paid by people who land there. ## What moves which number This is the diagnostic value of splitting the two figures. - Adding an import inside one page's client component moves **that route's `Size`** and its First Load JS. - Adding an import to the **root layout**, to a provider wrapped around the whole app, or to a module many routes already use, moves the **shared** number — and therefore every route at once. A jump in the shared line is the loudest signal in the whole build output, because it is a regression for every entry point simultaneously. - Wrapping a component in `next/dynamic` should move the route's number down without changing the shared line, *provided* nothing else still imports that module eagerly. A useful habit is to record the shared number and the two or three highest routes for each release, so "it grew" is a fact rather than a hunch. ## What it does not include First Load JS counts JavaScript, and only the client bundle. It does not tell you about: - **CSS**, images, or fonts, which also block or delay rendering. - **Server-side code.** Modules used only by Server Components or route handlers do not appear in the client figure at all — which is precisely why moving work to the server shows up here as a drop. - **What is actually executed.** A chunk can be downloaded and largely unused; bytes shipped and bytes needed are different questions. - **Compression.** Interpret the figure consistently — compare build to build rather than arguing about the absolute number against numbers quoted elsewhere. ## Turning it into a check The number is only useful if someone looks at it. Two practical moves: - Print the build output in CI and eyeball the shared line on every release, or fail the build when it crosses a threshold you chose deliberately. - When a route regresses, do not guess: run the build with `@next/bundle-analyzer` enabled and look at the treemap for that route's chunk. The table tells you *that* something grew; the analyzer tells you *what*. ```bash ANALYZE=true npx next build ``` The pairing is the point — the route table is the alarm, the analyzer is the investigation.
- Why can you not sum the First Load JS column to get the app's total JavaScript?Because every row already contains the shared baseline, so summing counts those chunks once per route. The rows overlap by design — each answers "what does a cold visit to this route cost", not "what does this route uniquely contribute". For a true total, look at the client bundle treemap in the analyzer instead.
- A pull request raises the "shared by all" number by 40 kB. Why is that worse than a 40 kB rise on one route?Because it lands on every cold entry point at once. A leaf route's growth is paid only by users who land there; the shared baseline is on the critical path of every first visit, whichever page it starts on. Shared-line regressions usually trace to the root layout, a global provider, or a module that crossed the hoisting threshold.
- Would moving a data-formatting library from a Client Component into a Server Component show up in this table?Yes — as a drop. The table counts only client JavaScript, so a module that no longer has to reach the browser leaves the route's figure entirely. That is one of the clearest signals the build output gives you that a boundary move actually paid off.
saying these in an interview costs you the question
- Adds the per-route column to get a total bundle size
- Treats First Load JS as the cost of every navigation
- Assumes it includes CSS, fonts, and images
- Ignores the shared line because no single route looks bad
- Compares the number to figures from another project as an absolute standard