Why Vite Feels Instant: Native ESM Serving and Dependency Pre-Bundling Explained
Vite's speed comes from two decisions: serving source as native ES modules and pre-bundling dependencies once. Here's how both work, how to verify them, and where they break down.
01 Oct 2025, 02:57 UTC

If you've migrated a project from a bundler-based dev server to Vite, the first thing you notice is the start-up time: the server is ready before you can switch to the browser. The second thing you notice is that saving a file updates the page almost immediately, often without losing component state. Neither of these is magic — they come from two specific design decisions: serving source files as native ES modules, and pre-bundling dependencies once instead of on every request.
Understanding these two mechanisms matters because they explain both Vite's speed and its occasional rough edges, like a slow first start or a stale dependency cache.
The problem with bundler-first dev servers
Traditional dev servers (webpack-based setups, older Rollup configs) build a bundle of your entire application before the browser can see anything. As the project grows, that initial bundle takes longer, and every change triggers re-bundling of at least part of the graph. The feedback loop degrades in proportion to project size.
Vite flips this. Modern browsers already speak ES modules natively — they can fetch import App from './App.vue' directly over HTTP. Vite simply serves each source file as its own module request, transforming only what the browser can't parse (TypeScript, JSX, Vue SFCs) on demand. There is no full-bundle step to wait for, so server start time stays roughly constant as your app grows.
Why dependencies still get bundled
Native ESM serving works well for your source code, which you control. It's a poor fit for node_modules, for two reasons:
- Many packages ship as CommonJS, which browsers can't import natively.
- Packages like lodash-es consist of hundreds of small modules; importing them directly would trigger a waterfall of hundreds of HTTP requests.
So on the first vite dev run, Vite scans your imports and pre-bundles dependencies with esbuild into a handful of ESM files cached under node_modules/.vite. This is the one slow step: on a large dependency tree, first start can take several seconds. Subsequent starts reuse the cache and are near-instant, as long as your lockfile and config haven't changed.
A minimal config that shows the moving parts
Vite is convention-first — many projects need no config at all. But a small vite.config.js is enough to control aliasing and pre-bundling behavior:
// vite.config.js — run from your project root
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [react()], // enables Fast Refresh for React components
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // import from '@/components/...'
},
},
optimizeDeps: {
// Force a CJS-only package into pre-bundling if Vite misses it
include: ['some-cjs-only-package'],
},
});No special permissions are needed beyond normal project access. After editing the config, restart the dev server — Vite does not hot-reload its own config in all cases, and optimizeDeps changes require a restart to take effect.
How to verify it's actually working
Two quick checks confirm both mechanisms, using only browser dev tools:
- Native ESM serving: run
vite dev, open the Network tab, and reload. You should see individual requests for your source files (e.g.,/src/components/Header.tsx), not one giant bundle. Dependencies appear as requests under/node_modules/.vite/deps/. - Fast Refresh (HMR): edit a component's markup while the page is open. The UI should update without a full reload — check that local state (an open modal, typed form text) survives the edit. If the whole page reloads instead, HMR fell back; the browser console usually says why (a common cause is a file that exports both components and non-component values).
The trade-offs worth knowing
The design isn't free. First-run pre-bundling is a real cost on large dependency trees, and the cache can go stale in confusing ways — if you add or switch a dependency and see odd import errors, restarting with vite dev --force re-runs pre-bundling and resolves most of them. Also note that dev and production use different pipelines (esbuild-optimized deps in dev, a Rollup bundle for production builds), so it's worth running vite build and vite preview before shipping to catch the rare case where behavior differs.
The actionable takeaway: if your Vite dev server ever feels slow, don't reach for config tweaks first. Check whether you're accidentally defeating the model — importing a huge CJS library that isn't pre-bundled, or watching the Network tab for request waterfalls — and fix that specific import. The architecture is fast precisely because it does less work; the wins come from keeping it that way.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.