The Fast Path: Native ESM, On-Demand Transforms, and Dependency Pre-Bundling
Vite's development server is built around a specific request: can the browser receive the code for the page it needs without waiting for the entire application to be bundled first? This chapter follows that request through HTML, native ESM, Vite's transform pipeline, dependency pre-bundling, and the caches that make repeated work cheap. By the end, the reader should be able to tell which work is deliberately delayed, which work is prepared in advance, and why an apparently simple import can still trigger optimization.
The distinction matters because “on demand” can sound like “do nothing until the browser asks.” That is not Vite's design. The server does enough preparation to make browser imports valid, enough transformation to run source code, and enough bookkeeping to understand relationships. It avoids doing the same work for every file before the first page is useful.
Start with the browser's request
Consider a small application whose HTML entry points to a module that imports a component, a stylesheet, an image, and a package from npm. A bundle-first tool may begin by traversing the whole application graph, transforming its files, and producing a development bundle. The browser sees the page after that broad preparation completes.
Vite begins with the HTML entry as source. The browser asks for the HTML, finds a module script, and asks the Vite server for that module. Vite resolves the module's identity, loads it from the file system or from a plugin, transforms it into code the browser can execute, and returns the result. The browser then discovers the module's imports and requests those modules in turn. The amount of immediate work is tied to the path the browser is using, not automatically to every route and component in the repository.
This is possible because ESM gives the browser a native module boundary. An ESM import names a dependency, and the browser can request that dependency as another module. Vite does not have to combine every application file merely to make the first module addressable. It can preserve the source-level boundaries while adding the transformations that web applications need, such as TypeScript or JSX transpilation, CSS handling, asset URL conversion, and import rewriting.
The server's response is not just a file read. Vite's plugin pipeline resolves the requested identifier, loads content, and applies transformations. A plugin can provide a virtual module or compile a framework-specific file. Vite's own processing can recognize imports and convert them into URLs the browser can request. The result is still experienced by the browser as a module graph, but the server has inserted the compatibility work between the source graph and the browser's ESM rules.
That sequence creates a useful asymmetry. Source code that a developer changes frequently is transformed close to the moment it is needed. If the current route never imports a large feature, that feature need not be transformed before the route appears. On the other hand, the server cannot be equally literal about third-party packages. The npm ecosystem was not published under one browser-only module contract.
Why a bare import needs help
A browser cannot resolve an import such as import { formatDate } from 'date-library' by searching a project's node_modules directory. A bare package name has meaning to a package manager and to a build tool, not to a browser receiving an ordinary URL. Some packages also publish CommonJS or UMD, formats that do not directly satisfy the development server's native-ESM model.
Vite's dependency pre-bundling is the bridge across that boundary. It detects dependency imports, resolves the package, and produces an ESM-compatible module that the browser can request through a URL. For CommonJS packages, the process includes import analysis so named imports can work in common cases even when a package's exports were assigned in a way that is not statically obvious. The purpose is compatibility first: the browser needs a module it can load and the application needs the exports it expects.
The second purpose is request control. An ESM package may technically be browser-compatible but internally consist of a large number of small modules. If an application imports one utility from such a package and the browser must follow every internal import, the browser can issue a very large number of requests. Vite's documentation uses lodash-es, with more than 600 internal modules, to illustrate the problem. Pre-bundling can consolidate that dependency so the browser requests a much smaller optimized unit during development.
This is why Vite treats application source and dependencies differently without contradicting its on-demand model. Application source is the part of the graph the developer is actively changing, so preserving its module boundaries keeps transformations and updates local. Dependencies are usually more stable, and their package-format or internal-graph problems are shared by every page that imports them. Preparing them once protects every later request.
The result is not the production bundle. Dependency pre-bundling applies to development. It creates a development cache and rewrites dependency requests to that cache; the production build later applies its own graph analysis, chunking, compatibility target, and asset-delivery decisions. Confusing the two leads to bad debugging: a dependency that appears as one optimized file during development does not imply that production will emit the same file or the same chunk structure.
Cold start, cache hit, and late discovery
On a cold start, Vite has to learn enough about the application's entry path to identify dependencies that should be optimized. By default, it crawls HTML files, or configured input entries when those are supplied, and scans the imports it finds. The discovered packages become optimization entries. The optimizer then produces metadata and cached files that the development server can use while serving the browser.
A repeat request takes a different path. If the dependency cache is still valid, Vite can serve the already optimized module rather than repeating package resolution and bundling. Vite also uses strong browser caching for resolved dependency requests. A developer who changes a dependency locally may therefore see old code even after the file on disk has changed: the browser can be holding an immutable optimized response. The documented remedy is intentional rather than automatic—disable the browser cache while debugging and force Vite to re-bundle the dependencies.
The file-system cache has its own identity. Vite considers the package-manager lockfile, patch changes, relevant Vite configuration, and NODE_ENV when deciding whether optimization must run again. This is an important architectural point: a cache is only safe when the inputs that affect its meaning are part of its identity. A lockfile change can select a different package version; a configuration change can alter how a package should be processed; a mode or environment change can affect the generated code.
The lifecycle can also begin after the server is already serving pages. A plugin transform may create an import that the initial scanner could not see. A code path loaded only after navigation may reveal another dependency. When Vite encounters an import absent from the current optimized set, it can re-run dependency optimization and reload the page if the generated dependency set changes. That reload is a consequence of changing the URLs and metadata the browser is using, not evidence that every source file suddenly needed a full production build.
The following sequence shows the main paths. It deliberately separates the plugin pipeline, dependency optimizer, and cache: they cooperate, but they do not own the same state.
The diagram also explains why the first request can feel fast while a later navigation causes visible activity. The first route may have used only a small part of the source graph. A later route can reveal a new package, a plugin-generated import, or a linked dependency that was not in the initial scan. Vite preserves the ability to discover such imports, but discovery after the browser has begun loading creates coordination work.
Monorepos change the default boundary
A linked package in a monorepo is not always treated like an installed third-party dependency. Vite detects linked dependencies that resolve outside node_modules and, by default, treats them as source code. That choice is useful when the linked package is being edited alongside the application: its source can participate in the transformation and HMR path rather than being frozen into a dependency cache.
The choice depends on the linked package being usable as ESM. If it is not, the package can be included explicitly in dependency optimization. This is a small setting with a larger architectural meaning. “Dependency” is not only a question of where a package name appears in package.json; it is also a question of how often the code changes, which runtime format it exposes, and whether the development server should preserve or collapse its module boundaries.
The same reasoning applies to include and exclude. A large dependency with many internal modules, or a CommonJS dependency, is a candidate for inclusion. A small dependency that is already valid ESM may be left for the browser to load directly. An import generated by a plugin may need explicit inclusion because it was not visible during the initial source scan. These controls are not a ranking of “fast” and “slow” settings. They define which part of the graph the optimizer is allowed or required to own.
Vite also offers modes that narrow discovery, such as disabling automatic discovery and listing only explicit dependencies. Such a mode can make the boundary predictable, but it transfers responsibility to the configuration: a CommonJS-only package that is omitted is not made browser-compatible by the absence of a scan. The more manual the boundary, the more important it is that the team understands the imports crossing it.
What the fast path does not promise
The request-driven model reduces unnecessary initial work, but it does not remove request overhead. Every requested source module still has to be resolved, loaded, transformed, and delivered. The browser can encounter a chain in which it must request one module before learning which module comes next. If a transform is slow, the delay can appear as a browser waterfall even though the server started quickly.
This distinction is especially important for large applications. A project can have an almost immediate server process and a slow first page because the page itself imports many modules, uses expensive plugin transforms, or triggers a dependency optimization pass. Conversely, a warm cache can make a repeat page fast while a configuration or lockfile change makes the next start expensive. “Startup,” “first-page load,” and “hot update” measure different paths through the system.
Vite's performance guidance therefore recommends looking at the relevant layer. Browser extensions and disabled browser caches can distort measurements. Plugin hooks can delay startup or each requested transform. Implicit extension resolution can require several file-system checks. A barrel file that re-exports many modules can make the browser fetch and transform more files than the one API the application uses. Selective warmup can prepare frequently used files, but warming too much recreates the eager work the development model was designed to avoid.
There is also a correctness boundary around stale work. If a module is invalidated while it is being transformed—perhaps because a newly discovered dependency forces a reload—Vite must avoid committing the old result as if it were current. The current implementation tracks pending requests and invalidation timestamps for this reason. That behavior is invisible when everything is stable, but it is what prevents concurrency from turning a fast response into a stale response.
The next consequence of the graph
Vite's fast path is therefore a carefully managed compromise. The browser receives source modules as it asks for them. Dependencies are pre-bundled when package formats or internal fragmentation make direct loading impractical. Caches preserve work only while their inputs and browser state remain valid. Late discovery remains possible, but it can require a new optimization pass and a reload.
At this point, the server has accumulated more than transformed files. It has a graph of imports, cached results, and invalidation state. That accumulated knowledge is what makes the next operation possible: when a file changes, Vite can ask not only “what should be transformed again?” but also “which modules are allowed to accept this change?”