In a code-split single-page app, how do you work out how much JavaScript a cold first visit to one specific route actually downloads, and why is the size of that route's own chunk a misleading answer on its own?
answer
- a route is a set of chunks, not one
- runtime plus shared plus route plus nested
- deep link, not in-app navigation
- cold cache, compressed, transferred
- sequential chunks cost more than the sum suggests
basics
~20 sThe honest figure is the sum of every chunk that visit must fetch — runtime, shared and vendor chunks, the route's chunk, and anything it lazily pulls in before rendering. A route chunk's own size counts only the last of those.
solid answer
~50 sA route does not load in isolation. A cold visit downloads the entry and runtime chunk, whatever shared or vendor chunks the entry depends on, the route's own chunk, and any further chunks that route requests before it can render. The route chunk is often the smallest term in that sum, which is why quoting it alone flatters the number. Two ways to get the real one. From the build: the bundler's stats record lists the assets each entry point and chunk depends on, so you can compute the closure of chunks a route needs. Empirically, and more convincingly: load the route with an empty cache and a fresh profile, and total the transferred JavaScript. The empirical number also catches things the build record never sees — chunks fetched by application logic, and third-party scripts. Report it as compressed transferred bytes for a cold visit, and say which route entry you measured, because a deep link and an in-app navigation to the same route download different amounts.
go deeper
Know that loading a route pulls in more than one file: the app's shared code plus the route's own chunk. Be able to open the network view on a fresh load and see several JavaScript requests, not one.
Explain which chunks make up a cold route load and why a nested lazy import is worse than its byte count suggests, since it cannot start downloading until the parent chunk executes.
Demonstrate a repeatable measurement: production build, deep link, empty cache, compressed transferred JavaScript, same conditions each run — and split the result into shared boot cost versus route-specific cost before proposing anything.
Own the framing that shared bytes are amortised across a session but paid in full on entry, and insist the split between boot cost and route cost is decided against real traffic patterns rather than an average over all routes.
## The question behind the question "How big is this route?" has no single answer until you say *for whom*. A first-time visitor deep-linking into `/settings` pays for everything needed to boot the app plus everything that route needs. A user who has been clicking around for five minutes pays only for the incremental chunk, because the shared parts are already in memory. Both numbers are real; they answer different questions, and mixing them up is how a team convinces itself a route is cheap when its cold-start cost is several hundred kilobytes. The attribution that matters for a landing metric is the **cold first visit to that URL**, because that is the visit that has to build a first screen from nothing. ## Why a chunk size is not the answer In a split app the loading graph for a route typically includes: - **The runtime/entry chunk** — the module loader and app bootstrap. Always paid. - **Shared or vendor chunks** the entry depends on — the framework, the router, common utilities. Always paid, and usually the largest single term. - **The route's own chunk** — the code the split boundary carved out. - **Anything the route pulls in before it can paint** — a nested lazy component, a data-formatting library imported inside the route, a chunk requested by the code that runs on mount. Only the third of those appears when someone points at a rectangle in a treemap and says "the settings route is 40 kB". The other three are frequently the bulk of the download. The last one is the sneakiest, because it is sequential: the route chunk has to arrive and execute before the browser learns it needs the next one. Two 50 kB chunks fetched one after the other are worse for the user than one 100 kB chunk, even though the byte totals match. Any per-route number that ignores request ordering will understate the experience. ## Getting the number from the build Bundlers record which assets belong to each entry point and which chunks each chunk depends on. Walking that graph gives you a static per-route total without running anything, which makes it cheap to compute for every route at once and easy to track over time. The limits are worth knowing: the static graph knows about split points the bundler created, but it cannot know which of them a given route reaches at runtime, and it knows nothing about scripts the application fetches by URL or third parties injected at runtime. ## Getting the number empirically Load the route directly — the real URL, not an in-app navigation — with an empty cache and a clean profile, and total the JavaScript that transferred. This is the number a stakeholder will believe, because it is what happened. It includes everything: framework, shared chunks, route chunk, nested chunks, runtime-fetched scripts, third parties. A few conditions make the measurement repeatable: - **Cold cache, every time.** A warm cache turns the measurement into noise. - **Deep link, not in-app navigation.** They exercise different loading graphs, and the deep link is the pessimistic, honest one. - **Compressed transferred bytes**, since that is what crossed the wire. - **Same environment each run** — production build, same host, ideally the same throttling profile if you also care about timing. Running the same measurement for a handful of representative routes produces the table that actually drives decisions: it shows which routes are expensive, and how much of each route's cost is shared boot versus route-specific code. ## Reading the result The split between shared and route-specific bytes is the interesting part. If a route's total is dominated by shared chunks, the route is not the problem and shrinking it will barely move anything — the boot path is what to look at. If a route carries a large route-specific chunk, the investigation is local, and the treemap for that one chunk will usually name the culprit in seconds. One more caveat: shared bytes are amortised across a session but paid in full on entry. A dependency used by six routes is efficient for a user who visits all six, and pure overhead for a user who lands on one and leaves. Which of those describes your traffic is a real question, and the field data — not the build output — is what answers it.
- Your measurement shows one route's total is almost entirely shared chunks. Where do you look next?At the boot path, not the route. When shared code dominates, shrinking the route's own chunk changes almost nothing, and the same shared cost is being paid by every other entry too. The productive move is to attribute what is inside the shared chunks and ask what is in there that the first screen does not need.
- Why measure a deep link rather than an in-app navigation to the same route?They exercise different graphs. An in-app navigation already has the runtime and shared chunks resident, so it measures only the incremental cost — useful, but it is the optimistic case. The deep link is what search traffic, shared links and hard refreshes actually do, and it is the number that maps onto first-visit loading metrics.
saying these in an interview costs you the question
- Quotes the route chunk size as the route's cost
- Measures with a warm cache and reports the number
- Measures by navigating inside the app instead of deep-linking
- Ignores chunks the route requests after it starts executing
- Assumes shared chunk bytes are free for a first-time visitor