Vite's Development Mode Mechanics
The first part provided an overview of Vite's feature set and integration steps. We now turn our attention to understanding the underlying logic of the tool, with the aim of answering "how it works" and guiding readers around common pitfalls.
Note: The scope of this article is strictly limited to Vite's development mode. For production builds, Vite switches to a bundling approach which is not discussed here.
Unbundled Development: The Core Concept
For a definition of 'unbundled development', please have a look on Part I, section I.
How Browsers Handle ES Modules
While browser ESM implementations come with a number of nuances, grasping the fundamentals of module importing is key to understanding Vite's strategy.
For a complete breakdown of browser ESM behavior, the Mozilla Team's in-depth deep dive is an excellent resource that explains the internal workings.
Modern browsers include a native mechanism to import modules directly from an HTML file via a script tag that sets the type attribute to module.
<script type="module" src="./main.js"></script>
Why is the
modulevalue necessary? Modules are fundamentally distinct from regular scripts and require separate treatment—see this explanation for further clarification.
Within any imported module, browsers also support the import syntax:
import App from “/js/app.js"
Just like the script tag, this triggers an HTTP request to retrieve the file app.js, which is automatically treated as a module rather than a standard script. This system facilitates the easy inclusion of nested dependencies, a pattern familiar to anyone working with bundled development.
When dependency graphs grow to multiple levels, module imports can generate numerous HTTP requests. This is why the approach is impractical for production use (a performance analysis is available in the previously cited article here).
The browser maintains a dictionary for all fetched modules, keyed by their URL, ensuring each module is loaded only once—provided the same module specifier is used for every import.
One final observation from these examples is that the module specifiers always begin with a /. This is because browsers currently lack support for bare imports, forcing us to use absolute or relative paths.
Observing Vite's Mechanism
Note: The following explanation pertains only to Vite's development mode. Vite still relies on bundling for production.
As established in part I, the strategy leverages the browser's native ESM support to directly load and instantiate the source code instead of delivering pre-built bundles.
To concretely understand this, consider the example of a top-level module (the entry point) from a standard Vue (3) application, exactly as served to the browser by Vite:
import { createApp } from "/@modules/vue";
import App from "/App.vue";
import "/index.css?import";
createApp(App).mount("#app");
"main.js" module served by Vite to the browser
<script type="module" src="./main.js"></script>
That's all there is to it. The code closely mirrors the original source, with only the module specifiers being rewritten.
From this top-level file, all other imports are resolved layer by layer, creating a chain of module imports (which are essentially HTTP requests).
Once the complete dependency graph is fetched, the browser begins instantiating modules in a bottom-up order, starting with leaf modules that have no dependencies and ending with the top-level script. In this example, all of Vue's dependencies are fully initialized before its bootstrap process runs.
Code Rewriting and Dependency Handling
What happens when Vite needs to handle assets that aren't native ES modules?
On-the-Fly Compilation of Resources
As mentioned in part I, every imported resource is converted into an ES module on the fly. This conversion happens inside a pipeline of Koa middleware functions, also referred to as "plugins", which share a design philosophy with Webpack's loaders.
Here you can find a full list of Vite's built-in plugins. Among them are:
- the
vuePlugin, which handles the transformation of.vuefiles by leveraging the Vue compiler to parse Single File Components (SFC) and generate render functions. - the
moduleRewritePlugin, which modifies module specifiers so they are browser compatible, and is also responsible for certain HMR-related tasks (further details are provided below).
Developers have access to a public API for adding their own custom plugins (which is essentially a transform function that Vite converts into a plugin). These are inserted near the beginning of the plugin processing chain.
A practical demonstration for creating your own plugin can be found in this article.
Dependency Resolution Strategy
For the dependencies meant to operate inside the browser (i.e., non-dev dependencies), Vite will search for an ESM distribution. If this isn't available, it will pre-bundle the dependency format into ESM, with the help of Rollup.
Since Vite operates as a Node application, it happily consumes CJS distributions for its dev-dependencies—the packages needed during tasks like testing and building.
Occasionally, Vite may encounter a dependency that it fails to transform into an ES module. Should that happen, the build either stops with a clear warning, or the runtime throws errors due to missing module resolutions.
The reason for these failures stems from the architectural divide between static vs. dynamic modules (see section C.). Nested modules are particularly challenging because they are harder to analyze statically.
Why Converting to ESM Can Be Complicated
Modules in ESM are static by design. Their dependencies must be declared and loaded before the module's code runs; they cannot be imported conditionally at runtime. Furthermore, module specifiers must always be plain string literals—they can't be variables whose values are determined during script execution.
Technically, it is possible to perform a dynamic import using the
import()function, but it executes asynchronously.
Dynamic module formats, in contrast, allow for synchonous imports during runtime, accepting specifiers based on values that may only be available at the time of execution. It's exactly this set of capabilities that makes moving a codebase from a dynamic to a static structure a non-trivial exercise.
The post provided in this link offers concrete examples of how static and dynamic systems differ.
Is it on the horizon for Vite to deliver complete support for such dynamic logic?
As indicated in this GitHub issue, a full implementation is unlikely. Supporting this fully would demand the creation of complex 'hacks'—a collection of code transformations and runtime utilities—that are not only somewhat precarious but would also go against Vite's core ESM-first philosophy. Instead, the project's direction is to steer the ecosystem forward by encouraging publishers to offer proper ES module versions.
Part I suggests several possible remedies for dealing with such dependency issues.
III. Browser updates at lightning speed:
Note: this section targets readers curious about the mechanics behind Vite's update process, offering limited practical value.
A. What enables near-instant updates:
Two key reasons account for Vite's ability to refresh a running application almost immediately after source files change:
- HMR support for Vue, React, and Svelte (via plugins)
- leaner code transformation (a byproduct of the unbundled development approach)
HMR (Hot Module Replacement) lets a live application update individual modules on the fly, sidestepping the need for a complete page reload (frequently preserving the app's state in the process). A spec proposal covering HMR, along with a solid overview, can be found here.
A critical point regarding HMR: Vite doesn't author the HMR logic for specific libraries or frameworks itself (except partially for Vue, covered below). Instead, it handles the generic scaffolding of the HMR pipeline and exposes a framework-neutral HMR API, making the tool largely agnostic to the underlying framework.
B. How HMR functions under the hood:
The mechanism works as follows:
- at startup, Vite establishes a 'hot context': it attaches a
hotproperty to theimport.metaobject (the ESM hook for adding module-level metadata) of every module that opts into HMR. This property exposes the API used to define HMR behavior for that module. Simultaneously, Vite builds an importer/importee relationship graph, which later guides the update procedure.
.vuefiles skip the HMR API entirely because Vite manages their HMR configuration natively.
- also at startup, the browser-side HMR is activated: Vite delivers a script that, at runtime, registers listeners and event handlers for HMR signals dispatched by the server. These handlers are responsible for 1. asynchronously fetching modules flagged by the server 2. applying those modules based on their hot context configuration.
- when a file changes, Vite determines which modules require re-import (relying on the importer/importee graph) and alerts the browser.
To identify those modules, Vite examines whether the changed file itself accepts HMR. If it doesn't, Vite traverses its importer chain upward, checking each ancestor until, for each import path, it locates a module that does accept HMR. All qualifying modules in that branch then get refreshed in the app. Should any import chain lack even one HMR-accepting module, Vite falls back to reloading the entire application.
For a deeper dive into HMR's internals, refer to this resource
As noted earlier, the client-side HMR layer—the callback portion that ultimately receives the fresh module and applies it to the running app—is intentionally outside Vite's scope. It delegates to external libraries: Vue's HMR module powers Vue, react-refresh handles React, and so on.
[
How should React apps be configured for HMR now that Fast Refresh supersedes react-hot-loader? · Issue #16604 · facebook/react
Dan Abramov suggested Devtools v4 would render react-hot-loader obsolete: https://twitter.com/dan_abramov/status/1144715740983046144?s=20 Me: I rely on this hook: require("react-reconciler…
GitHubfacebook

](https://github.com/facebook/react/issues/16604)
Closing thoughts
Appreciate you sticking with this! Vite remains young and marked as experimental, built on concepts we're not accustomed to in everyday workflows—my aim was to demystify these, hopefully without coming across as too dense!
In any case, I'm eagerly anticipating (and hopefully you share this sentiment!) what's next: continued development and smoother adoption into current projects. Perhaps even integration into frameworks down the line.
Regardless, one certainty stands: Vite is set to elevate the developer experience.
