Skip to content
AstroPaper
Go back

Vite — Ultimate Deep-Dive Guide

Updated:
Edit page

Vite — Ultimate Deep-Dive Guide

From beginner usage to build-system architecture and tooling internals.


Table of Contents

Open Table of Contents

1. Big Picture

What Vite Actually Is

Vite is not a bundler. It is a frontend build tool that orchestrates multiple lower-level tools:

Vite’s core insight: browsers can now natively import ES modules, so the dev server doesn’t need to bundle your source code. It only needs to transform individual files on demand.

Why Vite Exists

Traditional bundlers (Webpack, Parcel) bundle everything before serving — even during development. As projects grow:

Vite solves this by splitting the problem:

ConcernTraditional BundlerVite
Dev startupBundle everything → servePre-bundle deps → serve source as ESM
File changeRebuild affected chunksTransform single file, push HMR update
DependenciesBundle inlinePre-bundle once with esbuild (~100x faster)
Source codeBundle + transformServe as native ESM, transform on request
ProductionSame bundlerRollup (different tool, optimized for output)

Core Concepts Glossary

TermDefinition
Native ESMBrowser’s built-in import/export — no bundling needed for dev
Dependency pre-bundlingesbuild converts node_modules (CJS/UMD/scattered ESM) into single ESM files
Transform pipelineChain of plugin hooks that process source code (TS→JS, JSX→JS, etc.)
Module graphIn-memory DAG of all imported modules, their dependencies, and HMR state
HMR boundaryModule that calls import.meta.hot.accept() — updates stop propagating here
Plugin containerRollup-compatible hook execution engine that runs in both dev and build
SSRServer-side rendering — Vite can transform and execute modules in Node.js
Module invalidationMarking a module’s cached transform result as stale

Dev Server Request Lifecycle

Browser requests /src/App.tsx


┌─────────────────────────┐
│  Vite Dev Server (HTTP)  │
│  Connect middleware       │
└────────┬────────────────┘


┌─────────────────────────┐
│  Module Resolution        │
│  resolveId hooks          │
│  - resolve aliases        │
│  - resolve bare imports   │
│  - virtual modules        │
└────────┬────────────────┘


┌─────────────────────────┐
│  Load                     │
│  load hooks               │
│  - read file from disk    │
│  - virtual module content │
└────────┬────────────────┘


┌─────────────────────────┐
│  Transform Pipeline       │
│  transform hooks          │
│  - TypeScript → JS        │
│  - JSX → JS               │
│  - CSS modules             │
│  - Asset URL rewriting    │
└────────┬────────────────┘


┌─────────────────────────┐
│  Module Graph Update      │
│  - Track imports          │
│  - Track importers        │
│  - Set HMR boundaries    │
│  - Cache transform result │
└────────┬────────────────┘


  Response to browser
  (with import-rewritten URLs)

HMR Update Flow

File changes on disk (chokidar watches)


┌─────────────────────────┐
│  Determine affected       │
│  modules via module graph │
└────────┬────────────────┘


┌─────────────────────────┐
│  Walk importers upward    │
│  until HMR boundary found │
│  (module with hot.accept) │
└────────┬────────────────┘

         ├── Boundary found → send WebSocket update (partial)

         └── No boundary → full page reload

Production Build Flow

Entry points (index.html / config entries)


┌─────────────────────────┐
│  Rollup Build             │
│  - resolveId → load →    │
│    transform pipeline     │
│  - Build module graph     │
│  - Tree-shaking           │
│  - Code splitting         │
│  - Chunk generation       │
│  - Asset handling         │
│  - Minification           │
└────────┬────────────────┘


  Output: dist/
  - index.html (injected scripts)
  - assets/*.js (hashed chunks)
  - assets/*.css (extracted)
  - assets/* (static assets)

When Vite Is a Good Fit

When Vite Becomes Problematic

Bundler Comparison Overview

FeatureViteWebpackParcelTurbopackRspackRollupesbuild
Dev approachNative ESMBundleBundleNative ESMBundleN/AN/A
Dev speed⚡ Fast🐌 Slow🐢 Medium⚡ Fast⚡ FastN/AN/A
HMRWebSocket + ESMWebSocket + chunksWebSocketWebSocket + RSCWebSocketN/AN/A
Prod bundlerRollupWebpackSWCTurbopackRspackRollupesbuild
Plugin systemRollup-compatWebpack-specificParcel-specificWebpack-compatWebpack-compatRollupGo plugins
ConfigMinimalComplexZero-configNext.js-integratedWebpack-compatRollup-nativeMinimal
Tree-shakingRollup (excellent)GoodGoodGoodGoodExcellentBasic
Code splittingRollup chunksWebpack chunksAutoAutoWebpack chunksManual/autoLimited
SSRBuilt-in primitivesManualLimitedNext.js-integratedManualN/AN/A
MaturityMatureVery matureMatureEarlyGrowingVery matureMature

2. Learning Roadmap by Skill Level

Level 1 — Newbie

Goal: Use Vite comfortably for a React + TypeScript project.

Core Topics

  1. Create a Vite project:

    npm create vite@latest my-app -- --template react-ts
    cd my-app && npm install && npm run dev
  2. Understand vite.config.ts:

    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    
    export default defineConfig({
      plugins: [react()],
    });
  3. Dev server basics: vite starts a dev server at localhost:5173. It serves your source files directly — no bundling step.

  4. Environment variables:

    • .env, .env.local, .env.development, .env.production
    • Only VITE_ prefixed vars are exposed to client code
    • Access via import.meta.env.VITE_API_URL
    • import.meta.env.MODE, import.meta.env.DEV, import.meta.env.PROD
  5. Static assets:

    • public/ folder — served as-is at root, not processed
    • Import assets in code — get hashed URLs: import logo from './logo.png'
    • ?url, ?raw, ?worker suffixes for special handling
  6. Path aliases:

    // vite.config.ts
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
      },
    }

    Also update tsconfig.json paths for TypeScript:

    {
      "compilerOptions": {
        "paths": { "@/*": ["./src/*"] }
      }
    }
  7. CSS handling:

    • .css imports are injected as <style> tags in dev, extracted in build
    • .module.css — CSS Modules (scoped class names)
    • PostCSS — auto-detected from postcss.config.js
    • Sass/Less/Stylus — install preprocessor, import directly
  8. Basic plugins: Plugins are functions that return objects with hooks:

    plugins: [react(), somePlugin()];
  9. Build: vite build produces optimized output in dist/.

  10. Preview: vite preview serves the built output locally.

Common Beginner Mistakes

10 Beginner Exercises

  1. Create a React + TS Vite app and deploy to Vercel
  2. Add path aliases (@/components, @/utils) with TS support
  3. Use environment variables for API URL in dev vs production
  4. Import and display an image, SVG, and JSON file
  5. Add Tailwind CSS with PostCSS
  6. Create a CSS Module component with scoped styles
  7. Add Sass and create a theme variables file
  8. Configure the dev server to proxy API requests
  9. Build the app and analyze the output in dist/
  10. Add a custom index.html title per environment

Level 2 — Junior

Goal: Understand how Vite works under the hood and configure it effectively.

Core Topics

  1. Advanced vite.config.ts:

    import { defineConfig, loadEnv } from 'vite';
    
    export default defineConfig(({ command, mode }) => {
      const env = loadEnv(mode, process.cwd(), '');
      return {
        define: {
          __APP_VERSION__: JSON.stringify(env.npm_package_version),
        },
        server: {
          port: 3000,
          proxy: {
            '/api': {
              target: 'http://localhost:8080',
              changeOrigin: true,
            },
          },
        },
        build: {
          sourcemap: true,
          rollupOptions: {
            output: {
              manualChunks: {
                vendor: ['react', 'react-dom'],
              },
            },
          },
        },
      };
    });
  2. Dependency optimization:

    • Vite pre-bundles node_modules with esbuild on first run
    • Cached in node_modules/.vite/deps
    • optimizeDeps.include — force pre-bundle specific packages
    • optimizeDeps.exclude — skip pre-bundling (e.g., for linked packages)
    • WHY: CJS→ESM conversion, reduce HTTP requests (lodash has 600+ modules)
  3. HMR behavior:

    • React Fast Refresh via @vitejs/plugin-react
    • CSS changes are instant (injected style tags)
    • Full reload triggers: config change, HTML change, no HMR boundary found
    • import.meta.hot.accept() makes a module an HMR boundary
  4. Code splitting:

    // Automatic via dynamic import
    const LazyComponent = React.lazy(() => import('./HeavyComponent'))
    
    // Manual chunks
    build: {
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes('node_modules')) {
              return 'vendor'
            }
          },
        },
      },
    }
  5. Dynamic imports: import('./module.ts') creates a separate chunk. Vite handles this in both dev (native ESM) and build (Rollup chunks).

  6. Environment modes:

    • vite → development mode
    • vite build → production mode
    • vite build --mode staging → custom mode, loads .env.staging
    • vite --mode test → custom dev mode
  7. Asset handling strategies:

    • import img from './img.png' → URL (hashed in build)
    • import img from './img.png?url' → explicit URL
    • import raw from './file.txt?raw' → string content
    • import worker from './worker.ts?worker' → Web Worker
    • new URL('./img.png', import.meta.url) → works with dynamic paths
  8. SVG strategies:

    • Import as URL (default): import svg from './icon.svg' → URL string
    • vite-plugin-svgr → import as React component
    • ?react suffix with svgr plugin → import Icon from './icon.svg?react'
    • Inline SVG via ?raw → string to dangerouslySetInnerHTML (avoid)

Common Anti-Patterns

10 Mini Project Ideas

  1. Multi-page app with shared layout and per-page entry points
  2. Markdown blog with import.meta.glob for content loading
  3. Component library in library mode with TypeScript declarations
  4. Dashboard with lazy-loaded route modules and loading states
  5. App with Web Workers for CPU-intensive computation
  6. Internationalized app with dynamic locale loading
  7. Theme switcher using CSS variables + CSS modules
  8. API dashboard with proxy configuration and mock server
  9. App with WASM module integration
  10. Micro-frontend shell that lazy-loads remote components

Level 3 — Senior

Goal: Architect scalable Vite setups, write plugins, optimize builds, integrate with monorepos and CI/CD.

Plugin Architecture

Every Vite plugin is a Rollup plugin superset. Vite adds extra hooks:

import type { Plugin } from 'vite';

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    enforce: 'pre', // 'pre' | undefined | 'post'
    apply: 'serve', // 'serve' | 'build' | undefined (both)

    // Vite-specific hooks
    config(config, env) {
      /* modify config */
    },
    configResolved(config) {
      /* read final config */
    },
    configureServer(server) {
      /* add middleware */
    },
    transformIndexHtml(html) {
      /* transform HTML */
    },
    handleHotUpdate(ctx) {
      /* custom HMR */
    },

    // Rollup-compatible hooks
    resolveId(source, importer) {
      /* custom resolution */
    },
    load(id) {
      /* custom loading */
    },
    transform(code, id) {
      /* transform source */
    },
  };
}

Plugin Execution Order

Alias resolution

enforce: 'pre' plugins

Vite core plugins (resolve, CSS, assets, etc.)

Normal plugins (no enforce)

Vite build plugins

enforce: 'post' plugins

Vite post-build plugins (minify, manifest, etc.)

SSR Architecture

// vite.config.ts
export default defineConfig({
  ssr: {
    // Dependencies to bundle for SSR (default: externalized)
    noExternal: ['some-css-in-js-lib'],
    // Force externalize
    external: ['heavy-node-lib'],
  },
});

// server.js (Express example)
import { createServer as createViteServer } from 'vite';

const vite = await createViteServer({
  server: { middlewareMode: true },
  appType: 'custom',
});

app.use(vite.middlewares);

app.use('*', async (req, res) => {
  const template = await vite.transformIndexHtml(req.url, rawHtml);
  const { render } = await vite.ssrLoadModule('/src/entry-server.tsx');
  const appHtml = await render(req.url);
  const html = template.replace('<!--app-->', appHtml);
  res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
});

Monorepo Integration

// apps/web/vite.config.ts in a pnpm workspace
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    // Ensure linked packages resolve to source
    conditions: ['development', 'module'],
  },
  optimizeDeps: {
    // Don't pre-bundle workspace packages
    exclude: ['@myorg/ui', '@myorg/utils'],
  },
  server: {
    // Watch workspace packages for HMR
    watch: {
      ignored: ['!**/node_modules/@myorg/**'],
    },
  },
});

Build Optimization

export default defineConfig({
  build: {
    target: 'es2020',
    minify: 'terser', // or 'esbuild' (faster, less optimal)
    cssMinify: 'lightningcss',
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react')) return 'react-vendor';
            if (id.includes('@mui')) return 'mui-vendor';
            return 'vendor';
          }
        },
      },
    },
    reportCompressedSize: false, // faster builds in CI
    chunkSizeWarningLimit: 1000,
  },
});

Docker Optimization

# Multi-stage build
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build

FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

CI/CD Optimization

# GitHub Actions
- name: Cache Vite deps
  uses: actions/cache@v4
  with:
    path: |
      node_modules/.vite
      ~/.cache
    key: vite-${{ hashFiles('pnpm-lock.yaml') }}

- name: Build
  run: pnpm build
  env:
    NODE_OPTIONS: '--max-old-space-size=8192'

Library Mode

// vite.config.ts for a library
import { defineConfig } from 'vite';
import { resolve } from 'path';
import dts from 'vite-plugin-dts';

export default defineConfig({
  plugins: [dts({ rollupTypes: true })],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      formats: ['es', 'cjs'],
      fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: { react: 'React', 'react-dom': 'ReactDOM' },
      },
    },
  },
});

10 Production-Grade Project Examples

  1. Design system library with Storybook + library mode + npm publishing
  2. SSR React app with streaming HTML and API routes
  3. Monorepo with shared configs, component library, and 3 apps
  4. Multi-page marketing site with per-page JS bundles
  5. Dashboard with route-based code splitting and prefetching
  6. Cloudflare Workers app with Vite-built worker bundles
  7. Internal admin platform with plugin-based feature modules
  8. E-commerce storefront with SSR + edge caching
  9. CLI tool that uses Vite’s JS API for building user plugins
  10. Microfrontend host/remote architecture with Module Federation

Level 4 — Expert

Goal: Understand Vite internals, debug complex build issues, architect large-scale systems.

Module Graph Internals

The module graph is a directed graph where:

EnvironmentModuleNode {
  url: '/src/App.tsx'
  id: '/absolute/path/src/App.tsx'
  file: '/absolute/path/src/App.tsx'
  type: 'js'
  importers: Set<EnvironmentModuleNode>     // who imports me
  importedModules: Set<EnvironmentModuleNode> // who I import
  acceptedHmrDeps: Set<EnvironmentModuleNode> // HMR accepted deps
  isSelfAccepting: boolean                    // has hot.accept()
  transformResult: { code, map, etag }        // cached result
  lastHMRTimestamp: number
  lastInvalidationTimestamp: number
}

Key operations:

HMR Invalidation Algorithm

1. File changes detected (chokidar)
2. Find module(s) mapped to that file
3. Invalidate transform cache for those modules
4. For each invalidated module:
   a. If module is self-accepting → update just this module
   b. If module is accepted by an importer → walk up to that importer
   c. If no boundary found after walking entire importer chain → full reload
   d. If circular dependency creates infinite walk → full reload
5. Collect all boundary modules
6. Send WebSocket message: { type: 'update', updates: [...] }
7. Client fetches fresh module via ?t=timestamp cache-busting
8. Client executes accept callback with new module

Plugin Pipeline Internals

Vite creates a Plugin Container that wraps Rollup’s plugin driver. In dev, it processes files on demand. In build, Rollup drives it.

Request → resolveId() chain → load() chain → transform() chain → serve

Each hook runs through all plugins in order:
  enforce:'pre' → normal → enforce:'post'

Hooks can:
  - Return null → pass to next plugin
  - Return value → stop chain (for resolveId, load)
  - Return transformed code → passed to next transform

Transform Lifecycle Detail

1. resolveId(source, importer, options)
   - Converts import specifier to absolute ID
   - Handles aliases, virtual modules, bare imports
   - First non-null return wins

2. load(id)
   - Reads file content (or generates virtual content)
   - Default: fs.readFile

3. transform(code, id)
   - All plugins run sequentially
   - TypeScript → JS (esbuild)
   - JSX → JS (esbuild or Babel)
   - CSS Modules → JS with class map
   - Each transform receives previous transform's output

Advanced SSR Internals

Vite’s SSR uses a separate module runner in Node.js:

Environment API (Vite 6+): Each environment (client, ssr, custom) gets its own module graph, plugin pipeline, and resolve conditions.

What Experts Care About That Juniors Miss

  1. Import chain depth — deep import chains slow HMR because invalidation walks up
  2. Barrel file explosionindex.ts re-exporting everything forces loading unused modules
  3. Pre-bundling cache invalidation — lockfile changes, config changes, include changes trigger full re-prebundle
  4. Chunk deduplication — shared dependencies between chunks can duplicate if not configured
  5. Source map qualitysourcemap: true in production doubles build time and output size
  6. CSS ordering — non-deterministic CSS chunk ordering in code-split apps
  7. Module resolution differences — dev (native ESM conditions) vs build (Rollup conditions) can diverge
  8. Transform caching304 Not Modified responses depend on ETag, If-None-Match
  9. Worker modules — separate build context, don’t share module graph
  10. Environment-specific behavior — plugins must handle both serve and build commands

15 Advanced Engineering Discussion Topics

  1. How does Vite’s module graph differ from Webpack’s dependency graph?
  2. What are the trade-offs of native ESM dev vs. bundled dev (Turbopack approach)?
  3. When does dependency pre-bundling become a bottleneck?
  4. How should you architect HMR boundaries in a large app?
  5. What causes dev/prod behavior differences and how to prevent them?
  6. How does Rollup’s tree-shaking interact with barrel files?
  7. When should you use manualChunks vs let Rollup decide?
  8. How do CSS chunk ordering issues manifest and how to solve them?
  9. What is the correct architecture for SSR with streaming?
  10. How do module resolution conditions affect library compatibility?
  11. What causes the “optimized dependency changed” loop?
  12. How should plugins handle per-environment state?
  13. When is worker mode needed vs. SharedWorker vs. Service Worker?
  14. How does Vite’s file watcher strategy affect monorepo performance?
  15. What are the implications of Rolldown replacing Rollup in Vite?

Level 5 — Tooling / Bundler Engineer Mindset

Goal: Understand WHY build tools work the way they do. Think at the toolchain architecture level.

Dev Server Internals

Vite’s dev server is a Connect (Express-like) HTTP server that:

  1. Intercepts requests for .ts, .tsx, .jsx, .css, .vue, .svelte, etc.
  2. Runs the plugin pipeline (resolveId → load → transform)
  3. Rewrites bare imports: import React from 'react'import React from '/node_modules/.vite/deps/react.js?v=abc123'
  4. Adds HMR client injection to HTML
  5. Serves pre-bundled deps from .vite/deps/ with strong caching (max-age=31536000,immutable)
  6. Serves source files with 304 Not Modified based on ETag

Native ESM Philosophy

The fundamental insight: the browser is the bundler during development.

Traditional:  Source → [Bundler] → Bundle → Browser
Vite dev:     Source → [Transform] → ESM → Browser (browser resolves imports)

This means:

Browser Caching Behavior in Dev

ResourceCache StrategyWhy
Pre-bundled depsmax-age=31536000,immutableContent-hashed filename, changes = new URL
Source files304 Not Modified + ETagAllows instant cache validation
HMR updates?t=timestamp cache-bustForces fresh fetch after invalidation

File Watcher Internals

Vite uses chokidar to watch the project root (excluding node_modules, .git):

HMR Protocol

WebSocket messages between server and client:

// Server → Client
{ type: 'connected' }
{ type: 'update', updates: [{ type: 'js-update', path, acceptedPath, timestamp }] }
{ type: 'full-reload', path?: string }
{ type: 'prune', paths: string[] }
{ type: 'error', err: { message, stack, plugin, id, loc } }
{ type: 'custom', event: string, data: any }

Client behavior:

  1. Receive update → dynamic import module with ?t=timestamp → execute accept callback
  2. Receive full-reloadlocation.reload()
  3. Receive prune → execute prune callbacks for removed modules

Rollup Graph Internals

Rollup builds a module graph, then chunks it:

  1. Module graph phase: resolve → load → transform for every reachable module
  2. Chunk assignment: based on entry points, dynamic imports, and manualChunks
  3. Chunk optimization: merge small chunks, handle circular references
  4. Rendering: generate code per chunk, scope hoisting, banner/footer
  5. Output: write files with content hashes

Plugin Container Internals

PluginContainer {
  // Wraps Rollup's plugin driver
  // In dev: processes files on demand
  // In build: Rollup calls hooks

  resolveId(source, importer) {
    // Run through all plugin resolveId hooks in order
    // First non-null return wins
  }

  load(id) {
    // Run through all plugin load hooks
    // Default: fs.readFile
  }

  transform(code, id) {
    // Run through ALL plugin transform hooks sequentially
    // Each receives previous output
    // Source maps are composed
  }
}

AST Transform Pipelines

Source (.tsx)

    ▼ esbuild (TS strip + JSX transform) — FAST, no type checking

    ▼ Plugin transforms (e.g., auto-imports, macro expansion)

    ▼ Import analysis (rewrite bare imports, inject HMR preamble)

    ▼ Final module served to browser

In production, Rollup does a full AST parse for tree-shaking and scope hoisting.

Future of Frontend Tooling

TrendImplication
Rolldown (Rust Rollup)Will unify dev/build into one tool, massive speed gains
Import mapsBrowser-native dependency resolution, may reduce pre-bundling need
Rust/Go compilersSWC, esbuild, oxc — transform speed is no longer the bottleneck
Environment APIPer-environment module graphs enable better SSR, RSC, edge
Module RunnerRun Vite-transformed code in any runtime (Node, Deno, Workers)

3. Vite 8 Deep Dive

Note: As of the knowledge cutoff, Vite 6 is the latest stable release with Vite 7 in development. This section covers the evolution from Vite 5 → 6 → 7 and the architectural trajectory toward Vite 8. Update this section as Vite 8 is officially released.

Vite 5 → 6 Migration

Key changes in Vite 6:

  1. Environment API — The biggest architectural change. Each environment (client, ssr, custom) gets its own:

    • Module graph (EnvironmentModuleGraph)
    • Plugin pipeline
    • Resolve conditions
    • Dev server processing
  2. hotUpdate hook replaces handleHotUpdate:

    // Old (deprecated)
    handleHotUpdate({ server, modules }) {
      server.ws.send({ type: 'full-reload' })
    }
    
    // New
    hotUpdate({ modules }) {
      this.environment.hot.send({ type: 'full-reload' })
    }
  3. ModuleRunner replaces ssrLoadModule for running Vite-transformed code in target runtimes.

  4. Per-environment plugins with this.environment context in hooks.

  5. Deprecation warnings for server-level APIs:

    future: {
      removeServerModuleGraph: 'warn',
      removeServerTransformRequest: 'warn',
      removeServerHot: 'warn',
    }
  6. Node.js 18+ required (dropped Node 16).

Vite 6 → 7 Migration

Key expected changes:

Vite 7 → 8 Trajectory

Expected architectural direction:

WHY These Changes Matter

ChangeFor Frontend EngineersFor Plugin AuthorsFor Large Apps
Environment APISSR/RSC becomes first-classMust use this.environmentBetter isolation, fewer bugs
RolldownFaster buildsNew plugin hooks possibleUnified dev/prod behavior
ModuleRunnerBetter SSR DXNew runtime targetsEdge/Worker compatibility
Per-env pluginsClearer mental modelPer-env state managementReduced cross-env bugs

Migration Concerns

Monorepo: Update all apps simultaneously since shared configs and plugins must be compatible with new APIs.

CI/CD: Rolldown may change output chunk structure — update cache keys, asset patterns, and deployment scripts.

Plugin compatibility: Check plugin repos for Environment API support. Many plugins need updates for this.environment context.


4. Setup Guide

React + Vite + TypeScript — Production-Grade Setup

Project Structure

my-app/
├── public/
│   └── favicon.svg
├── src/
│   ├── assets/
│   ├── components/
│   │   └── ui/
│   ├── features/
│   │   └── auth/
│   │       ├── components/
│   │       ├── hooks/
│   │       ├── api/
│   │       └── index.ts
│   ├── hooks/
│   ├── lib/
│   ├── pages/
│   ├── styles/
│   │   ├── global.css
│   │   └── variables.css
│   ├── types/
│   ├── utils/
│   ├── App.tsx
│   ├── main.tsx
│   └── vite-env.d.ts
├── tests/
│   ├── setup.ts
│   └── utils.tsx
├── .env
├── .env.production
├── index.html
├── package.json
├── postcss.config.js
├── tailwind.config.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts

Scalable vite.config.ts

import { defineConfig, loadEnv, type PluginOption } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { resolve } from 'path';

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const isProd = mode === 'production';
  const isTest = mode === 'test';

  const plugins: PluginOption[] = [react(), tsconfigPaths()];

  return {
    plugins,

    resolve: {
      alias: {
        '@': resolve(__dirname, 'src'),
      },
    },

    server: {
      port: 3000,
      strictPort: true,
      proxy: {
        '/api': {
          target: env.VITE_API_URL || 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },

    build: {
      target: 'es2020',
      sourcemap: isProd ? 'hidden' : true,
      reportCompressedSize: false,
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes('node_modules')) {
              if (id.includes('react')) return 'react-vendor';
              return 'vendor';
            }
          },
        },
      },
    },

    css: {
      modules: {
        localsConvention: 'camelCase',
      },
      devSourcemap: true,
    },

    test: {
      globals: true,
      environment: 'jsdom',
      setupFiles: './tests/setup.ts',
      css: true,
    },

    define: {
      __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
    },
  };
});

Vitest Setup

// vitest.config.ts (or inline in vite.config.ts)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './tests/setup.ts',
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      exclude: ['node_modules/', 'tests/'],
    },
  },
});
// tests/setup.ts
import '@testing-library/jest-dom/vitest';

Tailwind CSS Setup

npm install -D tailwindcss @tailwindcss/postcss postcss
// postcss.config.js
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};
/* src/styles/global.css */
@import 'tailwindcss';

Storybook Setup

npx storybook@latest init --builder @storybook/builder-vite

Storybook auto-detects Vite and reuses your vite.config.ts.

SVG Setup

npm install -D vite-plugin-svgr
// vite.config.ts
import svgr from 'vite-plugin-svgr';

plugins: [
  react(),
  svgr({
    svgrOptions: { icon: true },
  }),
];
// Usage
import Logo from './logo.svg?react'; // React component
import logoUrl from './logo.svg'; // URL string

ESLint Setup

npm install -D eslint @eslint/js typescript-eslint eslint-plugin-react-hooks

Monorepo Integration (pnpm workspace)

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
// apps/web/vite.config.ts
export default defineConfig({
  optimizeDeps: {
    exclude: ['@myorg/ui'], // don't pre-bundle workspace packages
  },
  server: {
    watch: {
      ignored: ['!**/node_modules/@myorg/**'],
    },
  },
});

Docker Integration

FROM node:20-alpine AS base
RUN corepack enable

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod=false

FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN pnpm build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Multi-Environment Builds

# .env.staging
VITE_API_URL=https://staging-api.example.com
VITE_SENTRY_DSN=...

# Build for staging
vite build --mode staging

SSR Setup

// vite.config.ts
export default defineConfig({
  plugins: [react()],
  ssr: {
    noExternal: ['styled-components'], // bundle for SSR
  },
  build: {
    ssr: true, // when building SSR bundle
  },
});

Library Mode Setup

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      formats: ['es', 'cjs'],
      fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'react/jsx-runtime'],
    },
  },
});

Given your background (React, Next.js, Astro, TypeScript, monorepos, CI/CD, Docker, Cloudflare Workers):

monorepo/
├── apps/
│   ├── web/              # Vite + React SPA or SSR
│   ├── docs/             # Astro (uses Vite internally)
│   └── admin/            # Vite + React
├── packages/
│   ├── ui/               # Component library (Vite library mode)
│   ├── utils/            # Pure TS utils
│   ├── config/           # Shared Vite configs
│   │   ├── vite.base.ts
│   │   ├── eslint.config.js
│   │   └── tsconfig.base.json
│   └── types/            # Shared TypeScript types
├── workers/
│   └── api/              # Cloudflare Workers (Wrangler, not Vite)
├── pnpm-workspace.yaml
├── turbo.json            # or nx.json
└── package.json

Shared Vite config:

// packages/config/vite.base.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';

export const createViteConfig = (overrides = {}) =>
  defineConfig({
    plugins: [react(), tsconfigPaths()],
    build: {
      target: 'es2020',
      sourcemap: true,
      reportCompressedSize: false,
    },
    ...overrides,
  });

5. Vite Ecosystem Comparison

DimensionViteWebpackTurbopackRspackParcelRollupesbuildSWCRolldown
LanguageJS/TSJSRustRustJS/RustJSGoRustRust
PhilosophyNative ESM devBundle everythingIncremental computeWebpack-compat speedZero configES output qualityRaw speedTransform speedRollup-compat speed
Dev modeESM + pre-bundleFull bundleTurbo engineFull bundleFull bundleN/A (lib)N/A (lib)N/A (transform)ESM + pre-bundle
HMRExcellentGoodExcellentGoodGoodN/AN/AN/AExcellent
Build speedFastSlowVery fastVery fastMediumMediumVery fastVery fast (transform)Very fast
Tree-shakingExcellent (Rollup)GoodGoodGoodGoodExcellentBasicN/AExcellent
Plugin systemRollup-compatWebpack APIWebpack-compatWebpack-compatParcel APIRollup APIGo APIRust/JS APIRollup-compat
SSRBuilt-in primitivesManualNext.js nativeManualLimitedN/AN/AN/APlanned
MonorepoGoodGoodExcellent (Turbo)GoodLimitedN/AN/AN/AGood
Config complexityLowHighNext.js onlyMediumZeroMediumLowLowLow
EcosystemLarge, growingMassiveNext.js ecosystemGrowingMediumLargeMediumLargeEarly

When to Choose Each

ToolChoose WhenAvoid When
ViteNew projects, React/Vue/Svelte SPAs, libraries, SSR with frameworksLegacy Webpack plugin dependency, very large enterprise apps (today)
WebpackExisting large apps, need specific Webpack plugins, complex SSRNew projects (too slow), simple apps
TurbopackUsing Next.jsNot using Next.js (tightly coupled)
RspackMigrating from Webpack, need speed + Webpack compatNew projects without Webpack investment
ParcelQuick prototypes, zero-config needsProduction apps needing fine control
RollupLibraries, need best tree-shakingApplications (no dev server, HMR)
esbuildCustom tooling, transform step, extreme speedNeed tree-shaking quality, plugin ecosystem
RolldownFuture Vite integrationProduction use today (still maturing)

6. Plugin Architecture Deep Dive

Plugin Hook Lifecycle

Server start / Build start

├── config(config, env)           → Modify config before resolution
├── configResolved(config)        → Read final config
├── configureServer(server)       → Add dev server middleware
├── buildStart()                  → Build begins

│   For each module:
│   ├── resolveId(source, importer) → Custom module resolution
│   ├── load(id)                    → Custom module loading
│   └── transform(code, id)         → Transform source code

├── handleHotUpdate(ctx)          → Custom HMR handling (deprecated)
├── hotUpdate(options)            → Custom HMR handling (new)

│   Build end:
│   ├── generateBundle(options, bundle) → Inspect/modify output
│   ├── writeBundle(options, bundle)    → Post-write actions
│   └── closeBundle()                   → Cleanup

└── configurePreviewServer(server) → Add preview server middleware

Virtual Modules

import { exactRegex } from '@rolldown/pluginutils';

function virtualModulePlugin(): Plugin {
  const virtualId = 'virtual:app-config';
  const resolvedId = '\0' + virtualId;

  return {
    name: 'virtual-app-config',
    resolveId: {
      filter: { id: exactRegex(virtualId) },
      handler() {
        return resolvedId;
      },
    },
    load: {
      filter: { id: exactRegex(resolvedId) },
      handler() {
        return `
          export const config = {
            version: '${process.env.npm_package_version}',
            buildTime: '${new Date().toISOString()}',
          }
        `;
      },
    },
  };
}

Custom Transform Plugin

function autoImportPlugin(): Plugin {
  return {
    name: 'auto-import',
    enforce: 'pre',
    transform(code, id) {
      if (!id.endsWith('.tsx') && !id.endsWith('.ts')) return;

      // Example: auto-import React if JSX is used
      if (code.includes('<') && !code.includes("from 'react'")) {
        return {
          code: `import React from 'react';\n${code}`,
          map: null,
        };
      }
    },
  };
}

Custom HMR Plugin

function hmrNotifyPlugin(): Plugin {
  return {
    name: 'hmr-notify',
    hotUpdate({ file, modules, timestamp }) {
      if (this.environment.name !== 'client') return;

      if (file.endsWith('.css')) {
        // CSS-only update — let Vite handle it
        return;
      }

      if (file.includes('/config/')) {
        // Config files changed — force full reload
        const invalidated = new Set();
        for (const mod of modules) {
          this.environment.moduleGraph.invalidateModule(
            mod,
            invalidated,
            timestamp,
            true,
          );
        }
        this.environment.hot.send({ type: 'full-reload' });
        return [];
      }
    },
  };
}

Dev Server Middleware Plugin

function apiMockPlugin(): Plugin {
  return {
    name: 'api-mock',
    configureServer(server) {
      // Pre-middleware (runs before Vite's internal middleware)
      server.middlewares.use('/api/health', (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ status: 'ok' }));
      });

      // Post-middleware (runs after Vite's internal middleware)
      return () => {
        server.middlewares.use((req, res, next) => {
          // Fallback handling
          next();
        });
      };
    },
  };
}

HTML Transform Plugin

function injectMetaPlugin(): Plugin {
  return {
    name: 'inject-meta',
    transformIndexHtml(html) {
      return {
        html,
        tags: [
          {
            tag: 'meta',
            attrs: { name: 'build-time', content: new Date().toISOString() },
            injectTo: 'head',
          },
          {
            tag: 'script',
            children: `window.__APP_VERSION__ = '${process.env.npm_package_version}'`,
            injectTo: 'head-prepend',
          },
        ],
      };
    },
  };
}

Plugin for Library/Monorepo: Auto-External

function autoExternalPlugin(patterns: RegExp[]): Plugin {
  return {
    name: 'auto-external',
    apply: 'build',
    config() {
      return {
        build: {
          rollupOptions: {
            external: (id) => patterns.some((p) => p.test(id)),
          },
        },
      };
    },
  };
}

Plugin Debugging Tips

function debugPlugin(): Plugin {
  return {
    name: 'debug',
    resolveId(source, importer) {
      console.log(`[resolve] ${source} from ${importer}`);
    },
    load(id) {
      console.log(`[load] ${id}`);
    },
    transform(code, id) {
      console.log(`[transform] ${id} (${code.length} chars)`);
    },
  };
}

Use DEBUG=vite:* environment variable for Vite’s internal debug logs:

DEBUG=vite:resolve,vite:transform vite

7. Cheatsheet

vite.config Patterns

// Function config (access command/mode)
export default defineConfig(({ command, mode }) => ({ ... }))

// Load env
const env = loadEnv(mode, process.cwd(), '')

// Conditional plugins
plugins: [react(), isProd && legacy()].filter(Boolean)

// Proxy
server: { proxy: { '/api': 'http://localhost:8080' } }

// Aliases
resolve: { alias: { '@': resolve(__dirname, 'src') } }

// Define globals
define: { __VERSION__: JSON.stringify('1.0.0') }

Environment Variables

FileLoaded When
.envAlways
.env.localAlways (gitignored)
.env.developmentvite (dev)
.env.productionvite build
.env.[mode]--mode [mode]
.env.[mode].local--mode [mode] (gitignored)

Priority: .env.[mode].local > .env.[mode] > .env.local > .env

Dynamic Import Patterns

// Route-level code splitting
const Page = React.lazy(() => import('./pages/About'));

// Glob import
const modules = import.meta.glob('./modules/*.ts');
// Returns: { './modules/a.ts': () => import('./modules/a.ts'), ... }

// Eager glob
const modules = import.meta.glob('./modules/*.ts', { eager: true });

// Glob with named export
const modules = import.meta.glob('./modules/*.ts', { import: 'setup' });

Asset Handling

import imgUrl from './img.png'; // → URL
import rawText from './data.txt?raw'; // → string
import workerUrl from './w.ts?worker'; // → Worker
import wasmInit from './m.wasm?init'; // → init function

// Dynamic asset URL
new URL(`./assets/${name}.png`, import.meta.url).href;

Common Commands

vite                          # Dev server
vite build                    # Production build
vite preview                  # Preview production build
vite build --mode staging     # Build with custom mode
vite optimize                 # Re-run dep optimization
vite --debug hmr              # Debug HMR
DEBUG=vite:* vite             # All debug logs

Common Errors & Fixes

ErrorCauseFix
require is not definedCJS module in ESM contextAdd to optimizeDeps.include
process is not definedNode global used in browserUse define: { 'process.env': {} }
Pre-bundling loopDep keeps changingPin with optimizeDeps.include
HMR full reloadNo HMR boundary foundCheck component exports, ensure React Fast Refresh rules
Different dev/prod behaviorConditional module resolutionCheck resolve.conditions, ssr.noExternal
CORS in devAPI on different originConfigure server.proxy
Module not found after buildDynamic import path not staticUse import.meta.glob instead

Build Optimization Tips

build: {
  target: 'es2020',                    // Modern targets = smaller output
  minify: 'esbuild',                   // Faster than terser
  cssMinify: 'lightningcss',           // Fastest CSS minification
  reportCompressedSize: false,         // Skip gzip calc in CI
  sourcemap: false,                    // Halves build time
  rollupOptions: {
    output: {
      manualChunks: { vendor: ['react', 'react-dom'] },
    },
    treeshake: { moduleSideEffects: false }, // Aggressive tree-shake
  },
}

Docker Optimization Tips

CI/CD Patterns

# Cache Vite pre-bundled deps
- uses: actions/cache@v4
  with:
    path: node_modules/.vite
    key: vite-deps-${{ hashFiles('pnpm-lock.yaml') }}

# Parallel build + test
- run: pnpm build & pnpm test --run & wait

8. Real-World Engineering Mindset

Large React Apps

Problem: Bundle size grows linearly, initial load degrades.

StrategySmall (<50 routes)Medium (50–200)Large (200+)
Route splittingReact.lazyReact.lazy + prefetchLazy + prefetch + skeleton
Vendor chunksSingle vendorSplit by domainGranular per-library
Code splittingPer routePer featurePer feature + shared chunks

Senior choice: Route-based splitting with React.lazy, vendor chunk per major library, aggressive sideEffects: false in package.json, avoid barrel files.

Monorepos

Problem: Vite dev server must watch and transform workspace packages.

Strategy:

  1. Exclude workspace packages from optimizeDeps (they’re source, not deps)
  2. Add them to server.watch.ignored exclusion (so changes trigger HMR)
  3. Use vite-tsconfig-paths for alias resolution via tsconfig
  4. Shared base config in a packages/config workspace package

Hidden pitfall: Workspace packages using CJS will break native ESM dev server. Ensure all workspace packages use ESM.

Shared Component Libraries

Build strategy: Vite library mode with preserved modules (each component is a separate entry).

build: {
  lib: {
    entry: {
      index: 'src/index.ts',
      Button: 'src/components/Button.tsx',
      Modal: 'src/components/Modal.tsx',
    },
    formats: ['es'],
  },
  rollupOptions: {
    external: [/^react/, /^react-dom/],
    output: { preserveModules: true },
  },
}

WHY preserved modules: Consumers can tree-shake at the component level without relying on barrel file analysis.

SSR Apps

ApproachComplexityPerformanceBest For
Framework SSR (Next.js, Astro)LowHighMost apps
Vite SSR primitives + ExpressMediumMediumCustom SSR needs
Vite SSR + streamingHighVery highPerformance-critical apps

Senior choice: Use a framework (Astro for content sites, Next.js for dynamic apps). Only use Vite SSR primitives if you need custom server logic that frameworks don’t support.

Cloudflare Worker Integration

Challenge: Workers run in V8 isolates, not Node.js. Vite’s SSR assumes Node.js.

Strategy:

Environment Variable Management

ApproachSecurityDXBest For
.env filesLow (committed)HighDefaults, non-secrets
.env.localMedium (gitignored)HighLocal secrets
CI/CD env varsHighMediumProduction secrets
define in configMediumMediumBuild-time constants

Critical rule: VITE_ vars are embedded in client JS — NEVER put secrets there. Use server-side env vars for secrets, expose only via API.

Microfrontend Architecture

Problem: Multiple teams shipping independent frontend apps.

StrategyIsolationComplexityVite Support
Module FederationRuntimeHigh@originjs/vite-plugin-federation
iframeFullLowNative
Import mapsESM-levelMediumNative browser support
Build-time compositionNoneLowMonorepo + code splitting

Senior choice for most teams: Monorepo with code splitting. True microfrontends only when teams genuinely cannot coordinate releases.


9. Brainstorm / Open Questions

Build Systems (10)

  1. Why does Vite use two different tools (esbuild + Rollup) and what are the tradeoffs?
  2. What would change if Vite used a single bundler for both dev and prod?
  3. How does pre-bundling affect cold start time as dependency count grows?
  4. When should you ship ESM vs CJS from a library built with Vite?
  5. What’s the cost of sourcemap: true in CI pipelines?
  6. How do content hashes in filenames interact with CDN caching?
  7. What build artifacts should be cached between CI runs?
  8. How does reportCompressedSize: false affect build time?
  9. What happens when two dependencies require different versions of the same package?
  10. How would you design a build system for 50 frontend apps in a monorepo?

Performance (10)

  1. Why does initial page load sometimes feel slower with Vite dev than Webpack dev?
  2. How does the import waterfall problem manifest in large apps?
  3. What’s the performance difference between manualChunks strategies?
  4. How does CSS code splitting interact with render-blocking behavior?
  5. When does tree-shaking fail and what patterns prevent it?
  6. What’s the cost of barrel files on dev server performance?
  7. How does optimizeDeps.include affect dev server startup?
  8. What causes the “optimized dependency changed, reloading” loop?
  9. How do you measure and optimize Time to Interactive in a Vite app?
  10. What’s the performance impact of large import.meta.glob patterns?

HMR (10)

  1. Why does HMR become slow in large apps?
  2. What causes a full page reload instead of a hot update?
  3. How do circular dependencies affect HMR propagation?
  4. Why might React Fast Refresh fail silently?
  5. How does the HMR boundary concept affect component architecture?
  6. What happens when a CSS module import changes?
  7. How does HMR work differently for CSS vs JS?
  8. What’s the relationship between module graph depth and HMR speed?
  9. How does import.meta.hot.invalidate() differ from a full reload?
  10. Why might HMR state preservation fail between updates?

Plugin Architecture (10)

  1. When should a plugin use enforce: 'pre' vs 'post'?
  2. How do you prevent plugin transforms from running on node_modules?
  3. What’s the difference between resolveId and resolve.alias?
  4. How should plugins handle different environments (client vs SSR)?
  5. What’s the cost of a plugin that runs transform on every file?
  6. How do virtual modules work and when should you use them?
  7. What’s the plugin ordering when Vite plugins mix with Rollup plugins?
  8. How do you debug why a plugin is slowing down dev server?
  9. What state management patterns work for plugins across environments?
  10. How does the new hotUpdate hook differ from handleHotUpdate?

SSR (10)

  1. When should SSR be separated from client builds?
  2. What dependencies should be externalized vs bundled for SSR?
  3. How does CSS handling differ between client and SSR builds?
  4. What’s the correct way to handle browser APIs in SSR code?
  5. How does streaming SSR interact with code splitting?
  6. What’s the relationship between Vite SSR and React Server Components?
  7. How do you debug SSR hydration mismatches caused by build differences?
  8. When should you use ssr.noExternal vs ssr.external?
  9. How does the Environment API change SSR architecture?
  10. What’s the ModuleRunner and when would you use it directly?

Monorepos (10)

  1. How should workspace packages be handled by Vite’s dependency optimizer?
  2. What file watching strategy works for monorepo HMR?
  3. How do you share Vite configs across monorepo apps?
  4. What happens when workspace packages have different module formats?
  5. How do you prevent one app’s build from breaking another?
  6. What’s the correct TypeScript config structure for Vite monorepos?
  7. How does Turborepo/Nx caching interact with Vite builds?
  8. Should workspace packages be pre-built or consumed as source?
  9. How do you handle shared CSS/design tokens across workspace packages?
  10. What’s the correct testing strategy for shared packages vs apps?

DX (10)

  1. How do you keep dev server startup under 1 second?
  2. What Vite plugins improve DX the most?
  3. How should error overlay be customized for team-specific errors?
  4. What’s the best debugging setup for Vite + React?
  5. How do you create project templates with Vite?
  6. What dev server configuration improves team productivity?
  7. How should you handle TypeScript strict mode with Vite?
  8. What’s the cost of running type checking during dev?
  9. How do you integrate Vite with browser DevTools effectively?
  10. What Vite config patterns reduce “works on my machine” issues?

CI/CD (10)

  1. What Vite artifacts should be cached in CI?
  2. How do you run Vite builds in parallel across monorepo apps?
  3. What’s the optimal Docker build strategy for Vite apps?
  4. How do you handle env vars in CI without .env files?
  5. What build flags optimize CI build speed?
  6. How do you validate build output in CI?
  7. What’s the correct Vite build + deploy strategy for preview environments?
  8. How do you handle build failures gracefully in CD pipelines?
  9. What monitoring should exist for build performance regression?
  10. How do you handle Vite version upgrades across a monorepo in CI?

Runtime Architecture (5)

  1. How does Vite’s output interact with browser module caching?
  2. What’s the preload strategy for code-split chunks?
  3. How do Web Workers interact with Vite’s module graph?
  4. What’s the relationship between Vite and Service Worker architecture?
  5. How does import map support affect Vite’s future architecture?

10. Practice Questions

Beginner (25 Questions)

Q1. What command creates a new Vite + React + TypeScript project?

Q2. True or False: Vite bundles all your source code before serving it in development.

Q3. Which prefix must environment variables have to be exposed to client-side code?

Q4. What happens when you put a file in the public/ directory?

Q5. How do you access environment variables in Vite client code?

Q6. What is the default dev server port for Vite?

Q7. How do you add a path alias in vite.config.ts?

Q8. What CSS feature does Vite support out of the box without plugins?

Q9. What does vite build produce by default?

Q10. True or False: vite preview starts a development server with HMR.

Q11. How do you import an image and get its URL?

Q12. What file is Vite’s HTML entry point?

Q13. What does import.meta.env.MODE return during vite build?

Q14. True or False: You can use require() in Vite source files.

Q15. What config property sets the base public path for deployment?

Q16. How do you proxy API requests to a backend in dev?

Q17. Which file takes highest priority: .env or .env.local?

Q18. True or False: Vite automatically installs Sass when you import .scss files.

Q19. What does the define config option do?

Q20. What is the difference between import img from './img.png' and import raw from './file.txt?raw'?

Q21. What does import.meta.env.DEV return?

Q22. What plugin is needed for React support?

Q23. True or False: Changes to vite.config.ts trigger HMR.

Q24. How do you import a Web Worker in Vite?

Q25. What directory stores Vite’s pre-bundled dependencies?


Junior (25 Questions)

Q26. What tool does Vite use for dependency pre-bundling?

Q27. True or False: Vite uses Rollup for development mode.

Q28. What is an HMR boundary?

Q29. Your dependency keeps reloading the entire app during HMR. What should you investigate?

Q30. What does optimizeDeps.include do?

Q31. How does import.meta.glob work?

Q32. What causes Vite to trigger a full page reload instead of HMR?

Q33. What is the purpose of manualChunks in build config?

Q34. What’s the difference between vite build and vite build --mode staging?

Q35. True or False: Dynamic imports (import()) create separate chunks in production build.

Q36. What does ssr.noExternal do?

Q37. How do you conditionally apply a plugin only during build?

Q38. What is the relationship between import.meta.hot.dispose() and import.meta.hot.data?

Q39. Why might dependency pre-bundling fail?

Q40. What does build.target: 'es2020' control?

Q41. How do you force Vite to re-run dependency optimization?

Q42. What is the enforce property on a plugin?

Q43. True or False: CSS changes trigger a full page reload in Vite dev.

Q44. What does import.meta.hot.invalidate() do?

Q45. How do you create a CSS Module in Vite?

Q46. What’s the difference between server.proxy rewrite and changeOrigin?

Q47. What plugin hook runs when a file changes during dev?

Q48. True or False: import.meta.glob patterns are resolved at build time.

Q49. What does build.sourcemap: 'hidden' do?

Q50. How does sideEffects: false in package.json help Vite builds?


Senior (25 Questions)

Q51. You notice HMR is slow in your monorepo. vite --debug hmr shows deep importer chains. What architectural change would help?

Q52. Your production build creates 200+ small chunks. What’s the problem and how do you fix it?

Q53. A Vite plugin’s transform hook is called for every file including node_modules. How do you fix it?

Q54. What’s the correct way to share Vite configuration across a monorepo?

Q55. Your CI build takes 5 minutes for a Vite app. What optimizations would you apply?

Q56. How do you debug why a specific module is included in the production bundle?

Q57. What’s the difference between resolve.conditions in Vite vs Node.js package exports?

Q58. How should you handle CSS ordering issues in a code-split Vite app?

Q59. Write a plugin that adds build metadata to every HTML file.

Q60. What’s the correct Docker layer caching strategy for a Vite monorepo?

Q61. How do you configure Vite library mode to output both ESM and CJS with correct package.json exports?

Q62. Your app has a dev/prod behavior difference: a library works in dev but throws in production. What do you investigate?

Q63. How do you implement a Vite plugin that provides type-safe virtual modules?

Q64. What’s the optimal chunk strategy for a dashboard app with 50 routes?

Q65. How does Vite handle circular dependencies differently in dev vs build?

Q66. What plugin hook would you use to generate a service worker with the list of all built assets?

Q67. How do you profile Vite plugin performance?

Q68. True or False: build.rollupOptions.external and ssr.external serve the same purpose.

Q69. What’s the correct testing strategy when using Vitest with Vite path aliases?

Q70. How do you implement A/B testing that doesn’t increase bundle size for all users?

Q71. Write a plugin that logs build timing per hook.

Q72. How do you set up Vite for a multi-page app (MPA)?

Q73. What’s the difference between vite-plugin-inspect and DEBUG=vite:*?

Q74. How do you handle environment variables in a Docker build for a Vite app?

Q75. Your Vite build succeeds but the deployed app shows a blank page. What do you check?


Expert / Tooling Engineer (25 Questions)

Q76. Explain the difference between Vite’s module graph and Rollup’s module graph.

Q77. Why does Vite pre-bundle dependencies into single files?

Q78. How does Vite’s dev server determine when to send a full-reload vs HMR update?

Q79. What architectural problem does native ESM development solve that bundled development doesn’t?

Q80. True or False: Vite’s plugin container in dev mode runs all Rollup hooks in the same order as Rollup.

Q81. How does browser module caching interact with Vite’s dev server?

Q82. Design a plugin that implements incremental CSS extraction with source maps.

Q83. What are the implications of Rolldown replacing Rollup in Vite?

Q84. How does transformRequest work internally in Vite?

Q85. Why does HMR become slow in a large app with deep component trees?

Q86. What is the \0 prefix convention in virtual module IDs?

Q87. How would you implement a Vite plugin that provides compile-time macros (like Babel macros)?

Q88. Explain the Environment API architecture in Vite 6+.

Q89. What causes the “optimized dependency changed” infinite loop?

Q90. How does Vite’s file watcher (chokidar) handle performance in large monorepos?

Q91. Design a build performance monitoring system for a Vite monorepo.

Q92. What’s the difference between ModuleRunner and ssrLoadModule?

Q93. How would you architect Vite for a 100-app monorepo?

Q94. What are the trade-offs of serving source as ESM vs bundling for dev?

Q95. True or False: Vite’s transform pipeline composes source maps automatically.

Q96. How does scope hoisting in Rollup differ from Webpack’s module wrapping?

Q97. What’s the import.meta.hot protocol between client and server?

Q98. How would you implement a Vite plugin that provides React Server Components support?

Q99. What’s the fundamental architectural difference between Vite and Turbopack?

Q100. Design the ideal frontend build architecture for 2026 and beyond.


11. Personalized Recommendations

Which Concepts Matter Most for Your Stack

Given React + Next.js + Astro + TypeScript + monorepos:

  1. Plugin architecture — you’ll need to write/modify plugins for monorepo setups
  2. Library mode — building shared component libraries across apps
  3. Monorepo integrationoptimizeDeps, watch config, shared base configs
  4. Build optimization — chunk strategies, tree-shaking, CI speed
  5. SSR mental model — understand how Vite SSR works even if you use Next.js/Astro (they use Vite internally)
  6. Environment API — critical for understanding Astro’s multi-target builds

Advanced Topics to Prioritize

  1. Module graph internals (debug HMR issues in monorepos)
  2. Rollup chunk strategy (optimize production bundles)
  3. Plugin development (build internal tooling)
  4. Dependency optimization internals (fix pre-bundling issues)
  5. Dev/prod parity (prevent deployment bugs)

Common Mistakes Frontend Engineers Make with Vite

  1. Using barrel files everywhere (kills tree-shaking and HMR)
  2. Not configuring optimizeDeps for monorepo packages
  3. Using process.env instead of import.meta.env
  4. Not understanding that VITE_ vars are embedded in the client bundle
  5. Ignoring chunk size warnings
  6. Not testing production builds locally before deploying
  7. Over-splitting with too many dynamic imports
  8. Not configuring base for non-root deployments
  9. Using terser when esbuild minification is sufficient
  10. Not caching Vite artifacts in CI

From Application Developer to Tooling/Platform Engineer

  1. Week 1–2: Read Vite source code — start with packages/vite/src/node/server/index.ts
  2. Week 3–4: Write 3 plugins (virtual module, transform, HMR)
  3. Week 5–6: Build a shared Vite config package for your monorepo
  4. Week 7–8: Profile and optimize build performance across your apps
  5. Week 9–10: Implement custom dev server middleware for team DX
  6. Week 11–12: Study Rollup/Rolldown internals and chunk algorithms
  7. Week 13–16: Build an internal Vite plugin that solves a team-wide problem
  8. Week 17–20: Contribute to Vite open source (bug fix or feature)

60-Day Learning Plan

WeekFocusMilestone
1Vite fundamentals, config, dev serverCan configure any Vite project from scratch
2Build optimization, code splitting, assetsOptimized production build for existing app
3Plugin architecture, write first pluginWorking virtual module plugin
4HMR internals, module graphCan debug any HMR issue
5Monorepo integration, shared configsMonorepo running with shared Vite config
6SSR architecture, Environment APISSR prototype with custom server
7Library mode, component library buildPublished component library with Vite
8CI/CD optimization, Docker integrationCI pipeline under 3 minutes
9Read Vite source code (server, plugins)Understand dev server internals

Core Documentation

Bundler References

Testing & Tooling

Framework Integration

Deployment & Edge

Vite Source Code (Key Files)

Talks & Articles


13. Advanced Engineering Topics

Native ESM Architecture

The browser’s import statement triggers an HTTP request per module. This creates a request waterfall:

index.js imports → App.js imports → Header.js imports → Logo.js
                                  → Nav.js imports → NavItem.js
                → Router.js imports → Route.js

Each import is a sequential HTTP request in the worst case (browser parallelizes siblings, but depth is sequential). For node_modules, Vite pre-bundles to collapse deep dependency trees into single files. For source code, the waterfall is acceptable because:

  1. Files are local (< 1ms per request)
  2. Browser caches unchanged modules
  3. Only the current page’s modules are loaded

Module Graph Algorithms

Construction: Lazy — modules added when first requested. On transform, new imports are discovered and added as edges.

HMR propagation: BFS/DFS upward through importers until boundary found. The algorithm must handle:

Invalidation: Clearing transformResult on a module. Cascading invalidation walks importers and clears their results too (since they may inline or reference the changed module’s exports).

HMR Invalidation Deep Dive

// Simplified invalidation algorithm
function invalidateModule(
  mod: EnvironmentModuleNode,
  seen: Set,
  timestamp: number,
) {
  if (seen.has(mod)) return; // prevent cycles
  seen.add(mod);

  mod.transformResult = null;
  mod.lastInvalidationTimestamp = timestamp;

  // Cascade to importers that don't have HMR boundaries
  for (const importer of mod.importers) {
    if (!importer.acceptedHmrDeps.has(mod)) {
      invalidateModule(importer, seen, timestamp);
    }
  }
}

Build Graph Optimization

Rollup’s chunk generation:

  1. Entry chunks: One per entry point
  2. Dynamic chunks: One per dynamic import()
  3. Common chunks: Modules imported by multiple chunks are extracted
  4. Manual chunks: Override via manualChunks config

Optimization goals conflict:

Incremental Rebuild Strategies

Current Vite: Full Rollup rebuild for production. No incremental production builds.

Future (Rolldown): Persistent module graph, only re-transform changed modules, incrementally update chunk assignments. This would reduce rebuild time from O(n) to O(changed).

Rust-Based Bundlers Architecture

ToolLanguageRoleArchitecture
esbuildGoTransform + bundleSingle-pass, no AST plugins
SWCRustTransformAST-level plugins via WASM
RolldownRustBundleRollup-compatible, parallel transforms
oxcRustParse + transformFastest parser, linter, transformer
TurbopackRustBundle + dev serverIncremental computation engine

The trend: Rust for compute-intensive work (parse, transform, bundle), JavaScript for configuration and plugin authoring.

Toolchain Interoperability

                ┌─────────────────────┐
                │    Vite (orchestrator) │
                └──────┬──────────────┘

          ┌────────────┼────────────┐
          │            │            │
    ┌─────▼─────┐ ┌───▼────┐ ┌────▼────┐
    │  esbuild   │ │ Rollup  │ │  oxc    │
    │ (pre-bundle)│ │ (build) │ │(future) │
    └────────────┘ └────────┘ └─────────┘
          │            │            │
          └────────────┼────────────┘

              ┌────────▼────────┐
              │    Rolldown      │
              │ (unified future) │
              └─────────────────┘

Summary

Key takeaways:

  1. Vite is a build tool orchestrator, not a bundler — it coordinates esbuild, Rollup, and plugins
  2. The native ESM dev server is Vite’s core innovation — transforms on demand, no bundling
  3. Pre-bundling solves the dependency waterfall problem
  4. Plugin architecture is Rollup-compatible, making the ecosystem portable
  5. HMR works by walking the module graph upward to find boundaries
  6. Production builds use Rollup for optimal output (tree-shaking, scope hoisting)
  7. Environment API (Vite 6+) enables per-target module graphs and plugins

Next steps:

  1. Write your first Vite plugin (virtual module)
  2. Set up a monorepo with shared Vite config
  3. Profile your existing app’s build performance
  4. Read Vite’s dev server source code
  5. Experiment with library mode for shared packages

Advanced topics to continue:


Edit page
Share this post:

Previous Post
TypeScript ULTIMATE Deep-Dive
Next Post
Web Vitals — Ultimate Deep-Dive Guide