rgoussu@goussu: ~/library/frontend/build-ecosystem
~/library/frontend/build-ecosystem cat vite.md

Vite deep dive

# Vite's dev/prod split — a native-ESM dev server with esbuild pre-bundling, Rollup/Rolldown production builds — plus the plugin API, the framework ecosystem built on it, and Vitest.

Conceptsaved 2026-08-10 #typescript#javascript#build#tooling#vite

Overview

Vite's founding insight is that development and production are different problems and deserve different tools. In dev it doesn't bundle at all: source modules are served to the browser as native ESM, transformed on demand, with dependencies pre-bundled once by esbuild — so cold start is near-instant and HMR cost scales with the edit, not the app. For production it hands the whole graph to Rollup (now Rolldown) for the optimized, split, minified output the dev model can't provide. That split, plus a Rollup-compatible plugin API, made Vite the shared substrate almost every modern frontend framework builds on — the closest thing the frontend has to a default build tool.

Key points

  • The dev server serves, it doesn't bundle: each source file is transformed on request and cached; an edit invalidates one module and HMR swaps it. Bare imports from node_modules are pre-bundled with esbuild once (CJS → ESM, hundred-file packages collapsed to one) so the browser isn't hit with request waterfalls.
  • index.html is the entry: not a JS file — Vite crawls the module graph from the <script type="module"> tag, and the HTML is a first-class part of the build.
  • Production builds through Rollup / Rolldown: full-graph tree-shaking, route-level code splitting, CSS extraction, hashed filenames. Dev/prod parity is kept by sharing plugins and resolution logic across both modes.
  • The plugin system is Rollup's, extended: a Vite plugin is a Rollup plugin with extra hooks (configureServer, transformIndexHtml, handleHotUpdate) and an apply: 'serve' | 'build' switch — one plugin ecosystem covers both halves.
  • Config and env: vite.config.ts with defineConfig; import.meta.env exposes mode and .env variables (only VITE_-prefixed ones reach client code); define does compile-time constant replacement.
  • The framework substrate: SvelteKit, Astro, Nuxt, React Router (the Remix line), SolidStart and others are Vite plugins-plus-conventions rather than bespoke toolchains — one transform pipeline shared across the ecosystem.
  • The Environment API (v6+) generalizes the dev server beyond the browser: multiple environments (client, SSR, edge workers) with their own module graphs and resolution, in one server — what frameworks needed to stop maintaining forked dev machinery.
  • Vitest is the test-runner sibling: it reuses Vite's config, plugins and transform pipeline, so tests import exactly what the app builds — no parallel Babel/Jest transform universe to keep in sync.
  • Library mode (build.lib) turns Vite around: entry + formats out, dependencies externalized — though pure library work often reaches for tsup/tsdown instead.
  • The Rolldown arc: the VoidZero company (Vite's creator) is rebuilding the core in Rust — Rolldown replaces both esbuild's and Rollup's roles, one engine for pre-bundle and build; rolldown-vite is production-ready and merging into Vite as the default.

The two modes side by side:

flowchart LR
    subgraph dev["vite (dev)"]
        SRC["source modules"] -->|"transformed on demand, served as native ESM"| BROWSER["browser"]
        DEPS["node_modules deps"] -->|"esbuild pre-bundle, cached once"| BROWSER
    end
    subgraph prod["vite build"]
        SRC2["whole module graph"] --> RB["Rollup / Rolldown"]
        RB --> OUT["split, hashed, minified bundles + index.html"]
    end

Details

Everyday commands

Command Effect
vite Start the dev server (default port 5173) with HMR
vite build Production build to dist/
vite preview Serve the built dist/ locally — a sanity check, not a prod server
vitest Run tests through the same config and transform pipeline

Where Vite is not the answer

Next.js keeps its own vertically-integrated toolchain (swc + Turbopack); pure backend services don't need a dev server at all (see backend builds); and a library that is just "TS in, ESM + .d.ts out" is simpler on tsup/tsdown than on library mode. Everywhere else in frontend land, Vite is the default that needs a reason not to be chosen.

Examples

A minimal but realistic vite.config.ts:

import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    port: 5173,
    proxy: { '/api': 'http://localhost:8080' }, // dev-only backend proxy
  },
  build: {
    sourcemap: true,
    outDir: 'dist',
  },
});

Framework plugins slot into a plugins: [] array; everything else is convention.

Related