Turbopack is described as compiling incrementally and on demand in `next dev`. What work happens when, and why can the first visit to a route still take a noticeable moment even though the dev server started instantly?
answer
- nothing compiled until requested
- first hit per route pays for its graph
- cache keyed by inputs, recompute the stale
- edit cost tracks blast radius, not app size
- restart loses the in-process cache
basics
~20 sTurbopack's dev server compiles nothing up front: it compiles a route the first time you request it, so a cold route pays for its own module graph. After that, an edit recomputes only the cached units whose inputs changed and pushes an update, which is why edits feel instant while first visits do not.
solid answer
~50 sTwo properties are doing the work. First, dev compilation is **demand-driven**: starting the server does not compile the application, it compiles a route when a request for it arrives. That is why startup is near-instant on a huge app and why the first hit on `/dashboard` still costs real time — Turbopack is compiling that route's module graph right then, including everything it imports. Second, compilation is **incremental at fine granularity**: the engine caches individual units of work keyed by their inputs, so when you save a file it invalidates only that unit and the units downstream of it, recomputes those, and pushes an update to the browser. The cost of an edit tracks what you changed rather than how large the app is. The cache lives in the running dev server by default, so restarting it puts every route back to cold; persisting it across restarts has been an opt-in experimental capability rather than default behaviour.
go deeper
Know that the dev server compiles a route when you first open it rather than everything at startup, and that this is why the first visit to a page is slower than later edits to it.
Explain both halves of the model: demand-driven route compilation, and a cache of individual computation results keyed by inputs so an edit only recomputes what is downstream of it.
Demonstrate the operational reading: dev speed is not build speed, unvisited routes are unverified, and a slow edit loop after a shared-module change is the invalidation graph doing its job rather than a regression to chase.
Own the measurement standard. Decide what the team tracks — first-route time, edit-to-update time, CI build time — so bundler decisions rest on numbers that reflect real developer wait time instead of a startup banner.
## Demand-driven compilation When `next dev` starts on Turbopack, almost nothing is compiled. The server binds a port and waits. The unit of work is a **request**: ask for `/`, and Turbopack compiles `/` — its `page`, the `layout` files above it, everything those import, and the client components in that subtree. Ask for `/settings/billing` next and it compiles that route, reusing whatever the two routes share. This is why the two timings people report feel contradictory. Startup is nearly independent of application size, because size is not what is being paid for. The first request to any given route is *not* independent of size — it is proportional to that route's own module graph. A route that pulls in a chart library, a markdown renderer and a date library has to compile all of it before it can respond. The practical corollary: **a route nobody opened locally was never compiled locally.** A broken import or a type-level-only mistake in an unvisited route will not surface in dev; a production build, which compiles every route, will find it. That is not a Turbopack bug, it is the direct consequence of demand-driven dev compilation. ## Incremental at the level of individual computations The second property is what makes the *second* interaction fast. Turbopack is built on an incremental computation engine: compilation is modelled as a graph of small functions — parse this file, resolve this specifier, transform this module, assemble this chunk — each with its result cached and each recording what it depended on. When you save a file, the engine does not ask "which chunk contained this module, rebuild it." It marks the cached results whose inputs changed as stale, propagates that to whatever depended on them, and recomputes only those. Everything else is reused verbatim. So editing a leaf component's JSX recomputes a handful of units; editing a module imported by two hundred files recomputes far more. The cost tracks the blast radius of your change, not the size of the project. That recomputed output is then delivered to the browser as a hot update rather than a page reload, which is what makes the edit-to-pixel loop feel immediate. ## Where the cache lives By default the cache is **in the running dev-server process**. That has a consequence worth stating plainly in an interview: killing and restarting `next dev` throws it away, and every route is cold again. Persisting the cache to disk so a restart resumes warm has been developed as an opt-in, experimental capability rather than the default, so do not promise a team that restarts are free. Production builds are a different shape entirely. `next build` has no notion of "routes you happened to visit" — it compiles everything, minifies, and produces the deployable output. Build times therefore scale with the whole application, and the incremental engine helps within a single build far more than it helps across builds. ## What this changes about how you work ```bash next dev # cold start is fast; first hit per route compiles that route # edit a component # only the affected computations rerun, then an HMR update # restart the server next dev # cache is gone by default; routes are cold again ``` Three habits follow. Do not benchmark a dev server by its startup line alone — measure first-route time and edit-to-update time, because those are what an engineer actually waits on all day. Do not treat "dev runs clean" as evidence the app builds; run a real build in CI, which compiles routes your local session never touched. And when someone reports the dev server "got slow," ask what they edited: a change to a widely imported module legitimately costs more than a change to a leaf, and that is the model working as designed, not a regression.
- Why does editing one shared utility module feel slower than editing a leaf component?Because invalidation propagates along the dependency graph. Changing a leaf component invalidates the cached work for that module and the chunks containing it — a handful of computations. Changing a module that two hundred files import invalidates every unit downstream of it, so far more has to be recomputed before an update can be sent. The cost tracks the blast radius of the change, not the file's size.
- Does a fast dev server tell you anything about production build time?Very little. Dev is demand-driven and only ever compiles what you requested, so its speed reflects the slice of the app you touched. `next build` compiles every route, minifies, and produces deployable output, so build time scales with the whole application. Teams that judge bundler performance from the dev loop alone are routinely surprised by CI.
- If a route was never opened during a dev session, what have you not verified about it?Essentially everything the bundler would have told you: that its imports resolve, that its modules transform, that a loader or alias it needs is configured. Demand-driven compilation never touched it. This is one concrete reason a production build belongs in CI on every change rather than only before release.
It works like a spreadsheet: changing one cell does not recalculate the whole workbook, only the cells that read it and the cells that read those.
saying these in an interview costs you the question
- Says the dev server compiles the whole app at startup
- Thinks incremental means only the edited file is re-read, never its dependents
- Assumes the dev cache survives restarting next dev by default
- Concludes production build time from dev-server speed
- Treats a clean dev session as proof every route compiles