Vite Dev Server Internals
The dev server is Vite’s most distinctive feature. Unlike traditional bundlers that build everything upfront, Vite serves source files directly and transforms them on demand.
Core Idea
Traditional (webpack/Rollup):
All files -> Bundle -> Serve one big file -> Browser
Vite dev:
Browser requests file -> Transform just that file -> Serve it
(only transform what the browser actually imports)
This is why Vite dev startup is fast regardless of project size: it doesn’t process files you haven’t imported yet.
Request Pipeline
When the browser requests a module:
Browser: GET /src/App.tsx
|
v
1. Connect middleware stack
|
v
2. transformMiddleware
- Check module graph cache
- If cached and not invalidated: return cached
- If not cached:
|
v
3. pluginContainer.resolveId("/src/App.tsx")
- Resolve to absolute path: /project/src/App.tsx
|
v
4. pluginContainer.load("/project/src/App.tsx")
- Read file from disk (or virtual module)
|
v
5. pluginContainer.transform(code, id)
- Plugin chain transforms the code:
a. vite:oxc -> strip TypeScript types, transform JSX (OXC replaced esbuild in Vite 8)
b. @vitejs/react -> inject React Refresh wrapper
c. vite:import-analysis -> rewrite bare imports
|
v
6. Return transformed JS with correct Content-Type
|
v
Browser: receives valid ESM, executes it, requests more imports
Import Rewriting
The critical transform. Source code has bare imports:
// Your source code
import React from "react";
import { Button } from "@/components/Button";
Browsers can’t resolve these. Vite rewrites them:
// What the browser receives
import React from "/node_modules/.vite/deps/react.js?v=abc123";
import { Button } from "/src/components/Button.tsx";
This happens in packages/vite/src/node/plugins/importAnalysis.ts.
Module Graph
Vite maintains a graph of all loaded modules and their relationships:
ModuleGraph
|
+-- ModuleNode("/src/App.tsx")
| imports: ["/src/components/Button.tsx", "react"]
| importers: ["/src/main.tsx"]
| lastHMRTimestamp: 1709747200000
| transformResult: { code: "...", map: ... }
|
+-- ModuleNode("/src/components/Button.tsx")
| imports: ["react"]
| importers: ["/src/App.tsx"]
| ...
The graph serves two purposes:
- Caching: skip transforms for unchanged modules
- HMR: know which modules to invalidate when a file changes
Hot Module Replacement (HMR)
File changed on disk (fsWatcher)
|
v
1. Invalidate the module in the graph
|
v
2. Walk importers to find HMR boundary
(a component that accepts hot updates)
|
v
3. Send WebSocket message to browser:
{ type: "update", updates: [{ path: "/src/App.tsx", timestamp: ... }] }
|
v
4. Browser's HMR client re-imports the module with ?t=timestamp
|
v
5. React Refresh (or framework equivalent) replaces the component
without losing state
For your markdown-task-planner (Electron + React), this is what gives you instant feedback when editing React components.
Dependency Pre-Bundling (Optimizer)
Node modules use CommonJS and have many small files. Serving them as individual ESM modules would be slow (hundreds of HTTP requests for React alone). So Vite pre-bundles them:
First dev start:
1. Scan imports in your source files
2. Find all bare imports (react, zustand, lucide-react, etc.)
3. Bundle each dep into a single ESM file using Rolldown (replaced esbuild in Vite 8)
4. Cache in node_modules/.vite/deps/
5. Serve cached files for subsequent requests
Subsequent starts:
- Check if deps changed (via lockfile hash)
- If not: skip, use cache
- If yes: re-bundle
This is where contribution opportunity #21149 applies: if scanning or bundling takes >1 second, log progress so the user knows what’s happening.
The optimizer code lives in packages/vite/src/node/optimizer/.
Middleware Stack
The dev server is a Connect-compatible HTTP server with middleware:
1. cors - CORS headers
2. proxy - API proxy (if configured)
3. base - Base URL handling
4. servePublicMiddleware - Static files from public/
5. transformMiddleware - Module transforms (the big one)
6. serveStaticMiddleware - Static file fallback
7. htmlFallbackMiddleware - SPA fallback to index.html
8. indexHtmlMiddleware - Transform index.html
For electron-vite, the renderer process dev server is exactly this. The main process and preload scripts use different handling (they’re Node.js targets, not browser).
File System Watching
Vite uses chokidar to watch for file changes:
chokidar watches project files
|
File change detected
|
Check if it's a config file -> full restart
Check if it's an HTML file -> full reload
Otherwise -> HMR update
Your Projects’ Touchpoints
| Vite Subsystem | Where You Hit It |
|---|---|
| Transform pipeline | Every time Vitest runs a test (ai-skills-sync, mysukari.com) |
| Dev server + HMR | markdown-task-planner development |
| Import rewriting | Every dev session |
| Dep pre-bundling | First run after adding dependencies |
| Plugin container | @vitejs/plugin-react, vite-tsconfig-paths |