Skip to content

13 - Vite ​


What is Vite ​

Vite (French for "fast") is a build tool and dev server for modern frontend projects. Created by Evan You (creator of Vue). Framework-agnostic β€” works with React, Vue, Svelte, vanilla JS, etc.

Two main jobs:

  1. Dev server β€” serves files using native ES modules, instant startup
  2. Production build β€” bundles using Rollup under the hood

Why Vite Over Webpack ​

The Problem with Webpack ​

Webpack dev server startup:
1. Read ALL files in the project
2. Bundle EVERYTHING into memory
3. Apply ALL loaders and transforms
4. Serve the bundle
Time: 10-60 seconds (grows with project size)

When you edit a file:
1. Re-bundle the affected modules + dependencies
2. Send entire updated bundle to browser
Time: 1-10 seconds (HMR, but still reprocesses a lot)

How Vite Solves This ​

Vite dev server startup:
1. Pre-bundle node_modules with esbuild (one-time, cached)
2. That's it. No bundling of YOUR code.
Time: ~300ms (regardless of project size)

When you edit a file:
1. Only the changed file is re-transformed
2. Browser fetches ONLY that file via ESM import
Time: <50ms (instant HMR)

Key insight: Vite doesn't bundle your code during development. The browser loads files individually using native ES module import statements.


How Vite Works Internally ​

Dev Mode: Native ESM ​

Your code:
  import { Button } from './components/Button'

Traditional bundler:
  Resolves import β†’ reads Button.tsx β†’ transforms β†’ bundles into app.js β†’ serves

Vite:
  Browser requests /src/App.tsx
  Vite transforms it on-the-fly (JSX β†’ JS, TS β†’ JS)
  Browser sees: import { Button } from '/src/components/Button.tsx'
  Browser requests /src/components/Button.tsx
  Vite transforms that file too
  Each file = one HTTP request (HTTP/2 makes this fast)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browser                                          β”‚
β”‚                                                  β”‚
β”‚  <script type="module" src="/src/main.tsx">      β”‚
β”‚      β”‚                                           β”‚
β”‚      β”œβ”€β”€ import App from './App.tsx'              β”‚
β”‚      β”‚       β”œβ”€β”€ import Button from './Button'   β”‚
β”‚      β”‚       └── import hooks from './hooks'     β”‚
β”‚      └── import './index.css'                    β”‚
β”‚                                                  β”‚
β”‚  Each import = HTTP request to Vite dev server   β”‚
β”‚  Vite transforms on demand (only what's needed)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Dependency Pre-Bundling (esbuild) ​

Node modules are NOT ES modules (many are CommonJS). Vite can't serve them as-is.

Solution: On first run, Vite pre-bundles all node_modules dependencies using esbuild (10-100x faster than Webpack's JS-based bundlers).

First run:
  node_modules/react/ (1000+ files, CommonJS)
    ↓ esbuild pre-bundle
  node_modules/.vite/deps/react.js (single ESM file, cached)

Subsequent runs:
  Reads from cache β€” instant
  Cache invalidated only when package.json or lock file changes

Production Build: Rollup ​

Vite uses Rollup for production builds because:

  • Native ESM in production would mean hundreds of HTTP requests (slow)
  • Rollup has better tree shaking than esbuild
  • Rollup has a mature plugin ecosystem
  • Rollup produces smaller, more optimized bundles
bash
npm run build
# Runs: rollup (via vite build)
# Output: dist/
#   β”œβ”€β”€ index.html
#   β”œβ”€β”€ assets/
#   β”‚   β”œβ”€β”€ index-a1b2c3.js      (your code, hashed)
#   β”‚   β”œβ”€β”€ vendor-d4e5f6.js     (node_modules, hashed)
#   β”‚   └── index-g7h8i9.css     (extracted CSS, hashed)

Hot Module Replacement (HMR) ​

What: When you edit a file, only that module is replaced in the browser without a full page reload. State is preserved.

You edit Button.tsx:
1. Vite detects file change (chokidar file watcher)
2. Transforms only Button.tsx
3. Sends WebSocket message to browser: "Button.tsx changed"
4. Browser fetches new Button.tsx
5. React Fast Refresh swaps the component
6. NO page reload, state preserved

Vite's HMR is file-level, not module-graph level. Webpack recalculates the entire affected module chain. Vite just re-serves the one changed file.

React Fast Refresh (via @vitejs/plugin-react):

  • Preserves component state during edits
  • Only re-renders the changed component, not the whole tree
  • Falls back to full reload if the edit changes non-component code (hooks, utils)

vite.config.ts β€” Anatomy ​

ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  // ---- Plugins ----
  plugins: [
    react(),   // React Fast Refresh + JSX transform
  ],

  // ---- Dev Server ----
  server: {
    port: 3000,           // dev server port
    open: true,           // open browser on start
    proxy: {              // proxy API calls to backend
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },

  // ---- Build ----
  build: {
    outDir: 'dist',           // output directory
    sourcemap: true,          // generate source maps
    target: 'es2020',         // browser target
    rollupOptions: {
      output: {
        manualChunks: {       // custom code splitting
          react: ['react', 'react-dom'],
        },
      },
    },
  },

  // ---- Resolve ----
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),   // @ = src/
    },
  },

  // ---- CSS ----
  css: {
    modules: {
      localsConvention: 'camelCase',   // CSS modules: .my-class β†’ myClass
    },
  },

  // ---- Testing (Vitest) ----
  test: {
    globals: true,              // no need to import describe/it/expect
    environment: 'jsdom',       // DOM simulation
    setupFiles: './src/test-setup.ts',
  },
});

Environment Variables ​

bash
# .env              β€” all environments
# .env.local        β€” local overrides (gitignored)
# .env.development  β€” dev only
# .env.production   β€” production only
bash
# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
tsx
// Access in code β€” MUST be prefixed with VITE_
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE;       // 'development' or 'production'
const isDev = import.meta.env.DEV;       // true in dev
const isProd = import.meta.env.PROD;     // true in prod

TypeScript support β€” add to src/vite-env.d.ts:

ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

How CSS Works in Vite ​

Plain CSS ​

tsx
import './styles.css'; // injected as <style> tag in dev, extracted in build

CSS Modules ​

tsx
import styles from './Button.module.css';
// styles.primary β†’ "Button_primary_a1b2c"  (scoped class name)
<button className={styles.primary}>Click</button>

PostCSS ​

Vite reads postcss.config.js automatically. No extra config needed.

CSS Pre-processors ​

bash
npm install -D sass    # for .scss files
npm install -D less    # for .less files

Just install and import β€” Vite handles the rest.


Static Assets ​

tsx
// Importing images β€” returns the URL
import logo from './assets/logo.png';
<img src={logo} alt="Logo" />
// In dev: /src/assets/logo.png
// In build: /assets/logo-a1b2c3.png (hashed for cache busting)

// Public directory β€” served as-is, no processing
// public/favicon.ico β†’ accessible at /favicon.ico

Asset size threshold: Files < 4KB are inlined as base64 data URLs (fewer HTTP requests). Files >= 4KB are copied to dist/assets/ with hash.

Configure:

ts
build: {
  assetsInlineLimit: 4096, // bytes (default 4KB)
}

Common Vite Interview Questions ​

Q: Why is Vite faster than Webpack in development? A: Vite doesn't bundle during dev. It serves files as native ES modules β€” the browser loads them individually. Only changed files are re-transformed. Webpack bundles everything upfront and re-bundles on change.

Q: What does Vite use under the hood? A: esbuild for dependency pre-bundling (CJS→ESM, fast), Rollup for production builds (optimized output, tree shaking). In dev, Vite itself serves files with on-demand transforms.

Q: Why not use esbuild for production builds? A: esbuild is fast but its code splitting and CSS handling are less mature than Rollup's. Rollup produces smaller, better-optimized bundles. The Vite team is working on Rolldown (Rust-based Rollup replacement) for future versions.

Q: How does HMR work in Vite? A: File watcher detects change β†’ Vite re-transforms only that file β†’ sends WebSocket message to browser β†’ browser fetches new module β†’ React Fast Refresh swaps the component without losing state.

Q: What is dependency pre-bundling? A: node_modules packages are often CommonJS (not ESM). Vite pre-bundles them into single ESM files using esbuild on first run, then caches the result. This also reduces the number of HTTP requests (react = 1000+ files β†’ 1 file).

Frontend interview preparation reference.