| | |

Vite 01 ⚡ Why Vite — The unbundled dev server philosophy

⏱️ Estimated read: 10 min | |

Vite was created on a single observation: traditional bundlers process your entire application before the browser can display anything, and the larger the app, the longer you wait . Vite rejected that trade-off by splitting the work into two parts. Dependencies—the libraries that rarely change—are pre-bundled once using fast native tooling. Source code—the application code you edit constantly—is served on-demand over native ES modules, and the browser loads only what it needs for the current page . The dev server startup became nearly instant regardless of application size, and editing a file triggered a Hot Module Replacement update over native ESM without a full page reload or a rebuild wait .

Key point: Vite’s unbundled dev server does not bundle your source code during development. The browser requests modules individually, and Vite transforms each file as it is requested. Bundling is still used for production builds, where the network round-trip cost of unbundled ESM makes shipping the raw modules inefficient .


Why Vite exists

The cold-start problem.

A traditional bundler walks the entire dependency graph, merges every module into a bundle, and only then serves the result. A large application means a long wait before the first page appears, and every restart repeats the cost . Vite moves the graph traversal from the build tool to the browser’s runtime. The dev server starts a static server, and when the browser requests main.ts, Vite transforms that file and returns it. When the file imports another module, the browser requests that module, and Vite transforms it on demand . Startup time becomes largely independent of project size, because there is no full bundle to produce.

The pre-bundling problem.

Native ESM alone would flood the browser with requests for the hundreds of internal modules inside libraries like lodash-es. Vite solves this with dependency pre-bundling: esbuild converts CommonJS and UMD dependencies into ESM and merges the internal modules of a library into a single file . A bare import like import { createApp } from 'vue' is rewritten to a path under /node_modules/.vite/deps/, and the browser fetches one pre-built file instead of the library’s entire module tree . The pre-bundle is cached with a hash in the URL, so a changed dependency version invalidates the cache and triggers a re-bundle .

The HMR problem.

Hot Module Replacement in Vite does not rebuild the bundle. It maintains a module dependency chain, and when a file changes, Vite sends an update over native ESM that replaces only that module in the browser . The application state is preserved, and the update latency is measured in milliseconds rather than the seconds that a full rebuild would take . The browser’s native module system is the transport, and Vite’s only job is to transform the changed file and notify the client.

The production bundling problem.

Unbundled ESM is efficient during development because the browser caches the many small requests. In production, the nested imports create additional network round trips, and shipping the raw modules is inefficient . Vite therefore uses a bundler for vite build, producing optimized static assets. In the current architecture, that bundler is Rolldown, a Rust-based bundler that replaced the earlier esbuild-and-Rollup combination in Vite 8 . The production path is a bundle; the development path is not.


a. The unbundled dev server in practice

The dev server does not pre-process the source tree. It transforms files as the browser requests them.

# Start the Vite dev server
npx vite
# The server starts in milliseconds, regardless of the project size

When the browser requests the entry module, Vite returns the transformed source. The imports inside that module become browser requests for the imported files.

// src/main.ts
import { createApp } from 'vue' // Bare import: rewritten to the pre-bundled path
import App from './App.vue'     // Relative import: served as a separate module

createApp(App).mount('#app')

The vue import is a bare specifier that the browser cannot resolve, so Vite rewrites it to the pre-bundled dependency path. The ./App.vue import is a relative path that the browser requests directly, and Vite transforms the .vue file into JavaScript on demand .


b. The pre-bundling cache

The pre-bundled dependencies are stored under node_modules/.vite/deps with a content hash. The hash changes when the lockfile, the dependency versions, the optimizeDeps configuration, or the Vite version changes, and the browser fetches the new files because the URL changed .

// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    include: ['lodash-es'], // Pre-bundle this dependency explicitly
    exclude: ['@my/local-package'], // Skip the pre-bundling for a local ESM package
  },
})

The include option is necessary when a dependency is discovered only at runtime, such as a dynamic import that the initial scan did not detect. The exclude option is for a dependency that is already ESM and has few internal modules, where the pre-bundling would add an unnecessary layer .


c. The production bundle

The production build uses the bundler to produce optimized assets. The unbundled dev server is not the deployment artifact.

# Build for production
npx vite build
# The output is a set of bundled assets in dist/

The build produces a main chunk and any code-split chunks created by dynamic imports. The static imports are merged into the main chunk to reduce the HTTP requests, and the dynamic imports become separate chunks that load on demand . The result is the optimized static output that the CDN or the web server serves.


Complete Example Session

# ============================================
# PART 1: SCAFFOLD THE PROJECT
# ============================================
npm create vite@latest my-app -- --template react

# ============================================
# PART 2: INSTALL THE DEPENDENCIES
# ============================================
cd my-app
npm install

# ============================================
# PART 3: START THE DEV SERVER
# ============================================
npm run dev
# The server starts in milliseconds

# ============================================
# PART 4: OBSERVE THE PRE-BUNDLED DEPENDENCIES
# ============================================
ls node_modules/.vite/deps/
# The pre-bundled react and react-dom files

# ============================================
# PART 5: EDIT A COMPONENT
# ============================================
# Edit src/App.jsx and save
# The browser updates the module without a full reload

# ============================================
# PART 6: BUILD FOR PRODUCTION
# ============================================
npm run build
# The bundled assets are written to dist/

# ============================================
# PART 7: PREVIEW THE PRODUCTION BUILD
# ============================================
npm run preview

The seven parts demonstrate the unbundled dev server lifecycle: the scaffold, the install, the instant startup, the pre-bundled dependencies, the HMR update, the production build, and the production preview.


Quick Reference

Dev vs Production

AspectDev ServerProduction Build
Source codeServed on-demand over ESMBundled
DependenciesPre-bundled with esbuildBundled
StartupInstant, independent of app sizeBuild time depends on app size
HMRNative ESM updateNot applicable
OutputNo bundleOptimized static assets

Pre-Bundling

OptionPurpose
optimizeDeps.includePre-bundle a runtime-discovered dependency
optimizeDeps.excludeSkip the pre-bundling for a local ESM package
Cache locationnode_modules/.vite/deps
Cache invalidationLockfile, dependency version, config, Vite version

The Two Bundlers (Vite 8+)

StageBundler
Dependency pre-bundlingesbuild
Production buildRolldown

Best Practices

✅ Do This:

# Start the dev server with the default config
npm run dev                                                   # ✅

# Use the include option for a runtime-discovered dependency
optimizeDeps: { include: ['lodash-es'] }                       # ✅

# Use the exclude option for a local ESM package
optimizeDeps: { exclude: ['@my/utils'] }                       # ✅

# Build for production with the bundler
npm run build                                                 # ✅

# Preview the production build
npm run preview                                               # ✅

❌ Don’t Do This:

# Expect the dev server to bundle the source code
# It serves the modules on demand                                # ❌

# Ship the unbundled dev output to production
# Use the build command                                         # ❌

# Ignore the pre-bundling cache
# The changed dependency needs a re-bundle                       # ❌

# Pre-bundle a local ESM package that has few modules
# Use the exclude option                                         # ❌

# Assume the production build uses the same mechanism as dev
# The production uses the bundler                                # ❌

Common Pitfalls

PitfallWhy It HappensFix
The dependency error at runtimeThe dependency was not pre-bundledAdd it to optimizeDeps.include
The local package change is not reflectedThe package is pre-bundled and cachedAdd it to optimizeDeps.exclude
The browser floods the server with requestsA large dependency is not pre-bundledPre-bundle it
The production build is slowThe large app is bundledExpected; the bundler processes the full graph
The HMR update is slowThe module chain is largeExpected; the update is still faster than a rebuild

Real-World Examples

1. Start the dev server

npm run dev

2. Pre-bundle a dependency

optimizeDeps: { include: ['echarts/core'] }

3. Exclude a local package

optimizeDeps: { exclude: ['@my/utils'] }

4. The cache directory

ls node_modules/.vite/deps/

5. Build for production

npm run build

6. Preview the build

npm run preview

7. The source import

import { createApp } from 'vue'

8. The pre-bundled path

// Vite rewrites the import to /node_modules/.vite/deps/vue.js?v=...

9. The HMR update

// Edit a component and the browser updates the module

10. The production output

ls dist/assets/
# The main chunk and the code-split chunks

Visual

The Dev Server vs the Bundler

┌──────────────────────────────────────────────────────────────┐
│  DEV: ON-DEMAND; PRODUCTION: BUNDLED                         │
│                                                              │
│  Dev server:                                                 │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Browser requests a module                              │  │
│  │ → Vite transforms it and returns it                    │  │
│  │ → The browser requests the imports                     │  │
│  │ → The cycle repeats                                    │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Production build:                                           │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ The bundler walks the full graph                       │  │
│  │ → merges the static imports                            │  │
│  │ → splits the dynamic imports                           │  │
│  │ → minifies and writes the assets                       │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

The Pre-Bundling

┌──────────────────────────────────────────────────────────────┐
│  PRE-BUNDLING SOLVES THE REQUEST FLOOD                       │
│                                                              │
│  Without pre-bundling:                                       │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ import { x } from 'lodash-es'                          │  │
│  │ → the browser requests hundreds of internal modules    │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  With pre-bundling:                                          │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ import { x } from '/node_modules/.vite/deps/lodash-es.js│ │
│  │ → the browser requests one file                        │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

The Production Build

┌──────────────────────────────────────────────────────────────┐
│  THE PRODUCTION BUILD MERGES THE STATIC IMPORTS              │
│                                                              │
│  Static import (import ... from ...):                        │
│  └── merged into the main chunk                              │
│                                                              │
│  Dynamic import (import()):                                  │
│  └── a separate chunk, loaded on demand                      │
│                                                              │
│  The result: fewer HTTP requests and a smaller initial load  │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Dev serverUnbundled, on-demand over native ESM
DependenciesPre-bundled with esbuild
Source codeServed as separate modules
HMRNative ESM update
Production buildBundled (Rolldown in Vite 8+)
Pre-bundle cachenode_modules/.vite/deps
Pre-bundle invalidationLockfile, version, config, Vite version
Production bundling reasonNetwork round-trip cost of unbundled ESM

Key takeaways:

  • Vite’s dev server does not bundle the source code. It serves the modules on demand over native ESM, and the browser requests each module it needs. The startup time is nearly independent of the application size .
  • Dependencies are pre-bundled to prevent the request flood. esbuild converts the CommonJS dependencies to ESM and merges the internal modules into a single file, so the browser fetches one pre-built file per dependency .
  • HMR uses the native ESM transport. When a file changes, Vite sends an update for that module only, and the application state is preserved. The latency is in milliseconds rather than seconds .
  • Production still uses a bundler. The unbundled ESM creates additional network round trips, so the production build merges the static imports and splits the dynamic imports for the optimized output .
  • The pre-bundling cache is invalidated by the config changes. The lockfile, the dependency versions, the optimizeDeps configuration, and the Vite version all change the cache hash and trigger a re-bundle .
  • Vite 8 uses Rolldown for the production build. The esbuild-and-Rollup combination was replaced by a single Rust-based bundler that is 10–30× faster than Rollup .

Remember: Vite is fast in development because it does not bundle the source code. The browser does the graph traversal, and Vite transforms the files as they are requested. The dependencies are pre-bundled once, and the HMR updates a single module over native ESM. The production build is still bundled, because the unbundled ESM is inefficient over the network. The result is the instant dev server and the optimized production build, which is the split that made Vite the default build tool for the modern front-end.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!