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:

  1. Caching: skip transforms for unchanged modules
  2. 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 SubsystemWhere You Hit It
Transform pipelineEvery time Vitest runs a test (ai-skills-sync, mysukari.com)
Dev server + HMRmarkdown-task-planner development
Import rewritingEvery dev session
Dep pre-bundlingFirst run after adding dependencies
Plugin container@vitejs/plugin-react, vite-tsconfig-paths