Tools

Vite: The Lightning-Fast Build Tool

Mayur Dabhi
Mayur Dabhi
June 13, 2026
14 min read

If you've ever stared at a terminal waiting 30 seconds for your development server to start, or watched a full-page reload wipe out your application state during development, you've felt the pain that Vite was built to solve. Vite (French for "quick") is a next-generation frontend build tool created by Evan You — the same developer behind Vue.js — and it has fundamentally changed how developers think about build tooling. In this guide, we'll explore why Vite is so fast, how to use it, and how to configure it for real-world projects.

Vite's Speed Secret

Vite starts your dev server in under 300ms regardless of project size, because it skips bundling entirely during development. Dependencies are pre-bundled once with esbuild (written in Go, 10–100× faster than JS-based bundlers), while your own source files are served as native ES modules directly to the browser.

Why Traditional Bundlers Are Slow

To understand why Vite is fast, you need to understand why Webpack and similar tools are slow in development. Traditional bundlers must process your entire application before serving even a single page. For a large app with thousands of modules, this means:

The root cause is architectural: bundlers were designed when browsers couldn't understand ES modules natively. That's no longer true. Every modern browser has supported native ESM since 2018.

Bundle-Based (Webpack) Source files Bundle everything Browser Must bundle ALL files before serving Slow cold start · Slow HMR ESM-Based (Vite) node_modules Dependencies esbuild pre-bundle Browser cached your source Source files Vite server on-demand Files served as native ESM on demand Instant start · Instant HMR

Webpack bundles everything upfront; Vite serves files as native ES modules on demand

Getting Started with Vite

Vite ships with an official scaffolding CLI that sets up a project with sensible defaults in seconds. It supports React, Vue, Svelte, Lit, Preact, vanilla JS, and TypeScript out of the box.

1

Scaffold a new project

Run the create command and follow the interactive prompts to choose your framework and variant.

Terminal
# npm 7+
npm create vite@latest my-app

# yarn
yarn create vite my-app

# pnpm
pnpm create vite my-app

# Skip prompts: pass framework and variant directly
npm create vite@latest my-react-app -- --template react-ts
npm create vite@latest my-vue-app -- --template vue
npm create vite@latest my-svelte-app -- --template svelte-ts
2

Install dependencies and start the dev server

Navigate into the project, install, and start. The dev server will be ready almost instantly.

Terminal
cd my-app
npm install
npm run dev

# Output:
#   VITE v5.x.x  ready in 287 ms
#
#   ➜  Local:   http://localhost:5173/
#   ➜  Network: use --host to expose
3

Explore the project structure

Vite's scaffolded project is minimal and opinionated — no boilerplate noise.

Project structure (React + TypeScript template)
my-app/
├── public/          # Static assets copied verbatim to dist/
│   └── vite.svg
├── src/
│   ├── assets/      # Assets processed by Vite (hashed, optimized)
│   ├── App.tsx
│   ├── main.tsx     # Entry point — imported as native ESM
│   └── vite-env.d.ts
├── index.html       # Root HTML — Vite entry, not public/
├── vite.config.ts
├── tsconfig.json
└── package.json
index.html is the Entry Point

Unlike Webpack where you specify an entry JS file in the config, Vite treats index.html as the entry point. It parses the HTML for <script type="module"> tags and follows the import graph from there — mirroring how browsers actually load ESM applications.

How the Vite Dev Server Works

Understanding Vite's dev server architecture explains why it stays fast no matter how large your project grows. There are two categories of modules Vite treats differently:

Pre-bundled Dependencies (node_modules)

Packages from node_modules are pre-bundled once using esbuild when you first start the server (or when they change). esbuild is written in Go and compiles 10–100× faster than JavaScript-based tools. These pre-bundled packages are cached in node_modules/.vite/deps/ and served as single ESM files — solving the "10,000 small files" problem that would otherwise overwhelm the browser.

Source Files (your code)

Your own source files are never pre-bundled. When the browser requests a file, Vite transforms it on the fly (TypeScript, JSX, CSS modules, etc.) and serves it as an ES module. Only the files the browser actually requests get processed — so a large unused module contributes zero startup cost.

Hot Module Replacement (HMR)

Vite's HMR is precise: when you change a file, only that module is invalidated. The HMR update payload is tiny — just the changed module — regardless of how many other files exist in the project. This is why Vite's HMR feels instant even in very large apps where Webpack's HMR takes several seconds.

HMR API (optional manual control)
// Frameworks handle HMR automatically.
// Use the low-level API only for custom module state cleanup:

if (import.meta.hot) {
    // Preserve state when module updates
    import.meta.hot.accept('./store.ts', (newModule) => {
        // newModule is the updated module
        replaceStore(newModule.store)
    })

    // Clean up side effects (intervals, subscriptions, etc.)
    import.meta.hot.dispose((data) => {
        clearInterval(pollingId)
        data.pollingId = pollingId // carry state to next version
    })
}

Configuring Vite

Vite is configured via vite.config.ts (or vite.config.js). The defineConfig helper provides TypeScript intellisense for all options.

A typical vite.config.ts for a React project:

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

export default defineConfig({
    plugins: [react()],
    // Base public path when served in production
    base: '/',
    // Environment variables prefix (default: VITE_)
    envPrefix: 'VITE_',
})

Configure module resolution aliases to avoid long relative imports:

import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
    resolve: {
        alias: {
            // "@/components/Button" instead of "../../components/Button"
            '@': path.resolve(__dirname, './src'),
            '@components': path.resolve(__dirname, './src/components'),
            '@utils': path.resolve(__dirname, './src/utils'),
            '@hooks': path.resolve(__dirname, './src/hooks'),
        },
        // Try these extensions in order when no extension is given
        extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
    },
})

Configure the development server — port, proxy, CORS, HTTPS:

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        port: 3000,           // Default is 5173
        open: true,           // Open browser on start
        host: '0.0.0.0',      // Expose to network (for Docker, etc.)
        strictPort: true,     // Error instead of trying next port

        // Proxy API requests to your backend during development
        proxy: {
            '/api': {
                target: 'http://localhost:8000',
                changeOrigin: true,
                rewrite: (path) => path.replace(/^\/api/, ''),
            },
            '/graphql': 'http://localhost:4000',
        },

        // CORS headers
        cors: true,

        // HTTPS (generate a local cert first)
        // https: true,
    },
})

Control production build output, code splitting, and minification:

import { defineConfig } from 'vite'

export default defineConfig({
    build: {
        outDir: 'dist',           // Output directory
        target: 'es2020',         // Browser compatibility target
        minify: 'esbuild',        // 'esbuild' (fast) or 'terser' (smaller)
        sourcemap: true,          // Generate source maps

        rollupOptions: {
            output: {
                // Split vendor code into a separate chunk
                manualChunks: {
                    vendor: ['react', 'react-dom'],
                    router: ['react-router-dom'],
                },
                // Asset file naming
                chunkFileNames: 'assets/js/[name]-[hash].js',
                entryFileNames: 'assets/js/[name]-[hash].js',
                assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
            },
        },

        // Warn when a chunk exceeds this size (in kB)
        chunkSizeWarningLimit: 500,
    },
})

The Plugin Ecosystem

Vite's plugin API extends Rollup's interface, which means most Rollup plugins work with Vite out of the box. The ecosystem has grown quickly — here are the plugins every serious Vite project should know:

Official Framework Plugins

Terminal — installing official plugins
# React (JSX transform + Fast Refresh)
npm install -D @vitejs/plugin-react

# React with SWC (Rust-based transpiler, even faster)
npm install -D @vitejs/plugin-react-swc

# Vue
npm install -D @vitejs/plugin-vue

# Svelte
npm install -D @sveltejs/vite-plugin-svelte

Essential Community Plugins

Plugin What It Does Install
vite-plugin-pwa Zero-config PWA with service worker generation npm i -D vite-plugin-pwa
unplugin-vue-components Auto-import Vue components on demand npm i -D unplugin-vue-components
vite-plugin-compression Gzip/Brotli compress build output npm i -D vite-plugin-compression
rollup-plugin-visualizer Bundle size analysis with treemap visualization npm i -D rollup-plugin-visualizer
vite-plugin-svgr Import SVGs as React components npm i -D vite-plugin-svgr
vite-plugin-mock Mock API endpoints during development npm i -D vite-plugin-mock

Writing a Custom Vite Plugin

Vite plugins are just objects with hook functions. Here's a simple plugin that injects a build timestamp into your app:

plugins/vite-build-info.ts
import type { Plugin } from 'vite'

export function buildInfoPlugin(): Plugin {
    return {
        name: 'vite-build-info',

        // Runs once at the start of the build
        buildStart() {
            console.log(`Build started at ${new Date().toISOString()}`)
        },

        // Transform a specific file's source code
        transform(code, id) {
            if (id.endsWith('src/main.tsx')) {
                // Inject build time as a global constant
                return code.replace(
                    '__BUILD_TIME__',
                    JSON.stringify(new Date().toISOString())
                )
            }
        },

        // Runs after the build completes
        closeBundle() {
            console.log('Build complete!')
        },
    }
}

// Usage in vite.config.ts:
// import { buildInfoPlugin } from './plugins/vite-build-info'
// plugins: [react(), buildInfoPlugin()]

Building for Production

Vite's production build uses Rollup under the hood — not esbuild. This is an intentional architectural decision: esbuild is blazingly fast but doesn't yet support all of Rollup's advanced code-splitting and tree-shaking capabilities. The result is highly optimized, tree-shaken bundles with automatic CSS code splitting.

1

Run the build command

Vite compiles and bundles your entire application into the dist/ directory.

Terminal
npm run build

# Output:
# vite v5.x.x building for production...
# ✓ 42 modules transformed.
# dist/index.html                    0.46 kB │ gzip:  0.30 kB
# dist/assets/index-Bx7QLdoA.css    1.39 kB │ gzip:  0.72 kB
# dist/assets/vendor-DcY7Z0lL.js   141.74 kB │ gzip: 45.64 kB
# dist/assets/index-BjnUr2H5.js     3.21 kB │ gzip:  1.49 kB
# ✓ built in 1.38s

# Preview the production build locally
npm run preview
# ➜  Local:   http://localhost:4173/
2

Analyze your bundle

Use rollup-plugin-visualizer to understand what's in your bundle before shipping.

vite.config.ts — bundle analysis
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
    plugins: [
        react(),
        // Only run during build: generates stats.html
        visualizer({
            open: true,        // Auto-open in browser after build
            gzipSize: true,
            brotliSize: true,
            filename: 'dist/stats.html',
        }),
    ],
})
3

Deploy the dist/ folder

Vite builds are static files — deploy to Vercel, Netlify, Cloudflare Pages, S3, or any static host. Configure your server to redirect all 404s to index.html for client-side routing to work.

SPA Routing Gotcha

If you use React Router or Vue Router in history mode, configure your web server to serve index.html for all routes. On Nginx: try_files $uri $uri/ /index.html;. On Apache: use a .htaccess with RewriteRule ^ index.html. Without this, direct URL navigation causes 404 errors.

Environment Variables

Vite exposes environment variables to your client code through import.meta.env. Only variables prefixed with VITE_ are exposed to the browser — server-only secrets without the prefix stay private.

.env files
# .env (committed — shared defaults)
VITE_APP_TITLE=My App
VITE_API_BASE_URL=https://api.example.com

# .env.local (gitignored — local overrides)
VITE_API_BASE_URL=http://localhost:8000

# .env.production (used when building for production)
VITE_APP_TITLE=My App (Production)

# Server-only — NOT exposed to browser
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=super-secret-value
src/config.ts — accessing env vars
// Built-in Vite env vars
console.log(import.meta.env.MODE)     // "development" or "production"
console.log(import.meta.env.BASE_URL) // "/" by default
console.log(import.meta.env.DEV)      // true in development
console.log(import.meta.env.PROD)     // true in production

// Your custom variables
const apiBase = import.meta.env.VITE_API_BASE_URL
const appTitle = import.meta.env.VITE_APP_TITLE

// TypeScript: add type declarations to vite-env.d.ts
// to get autocomplete for your custom vars:
interface ImportMetaEnv {
    readonly VITE_API_BASE_URL: string
    readonly VITE_APP_TITLE: string
}

interface ImportMeta {
    readonly env: ImportMetaEnv
}
Vite Production Build Pipeline Source TS/JSX/CSS Rollup Bundle + Tree-shake esbuild Minify JS/CSS dist/ Hashed + compressed CDN / Server Asset hashing for long-term caching · Automatic CSS code splitting · Polyfills via @vitejs/plugin-legacy

Vite uses Rollup for bundling and esbuild for minification in production

Vite vs Webpack: When to Use Which

Vite is the right choice for most greenfield projects. But Webpack still has a role — particularly in large enterprise codebases with complex, custom build pipelines built up over years. Here's an honest comparison:

Feature Vite 5 Webpack 5
Dev server cold start <300ms (no bundling) 5–60s (full bundle)
HMR speed Instant (single module) Slow on large apps
Production bundler Rollup (excellent tree-shaking) Webpack (highly configurable)
Configuration complexity Minimal, sensible defaults Complex, verbose
Plugin ecosystem Growing, Rollup-compatible Mature, vast
CSS/asset handling Built-in (CSS Modules, PostCSS) Requires loaders config
Module Federation Via vite-plugin-federation Native support (v5)
TypeScript Zero-config (esbuild transpile) Requires ts-loader/babel
Legacy browser support Via @vitejs/plugin-legacy Native (target option)
Best for New projects, SPAs, SSR Complex legacy migrations, MF

Migrating an Existing Webpack Project to Vite

  1. Install Vite: npm install -D vite @vitejs/plugin-react (or your framework plugin)
  2. Move index.html: Move from public/index.html to the project root and replace %PUBLIC_URL% placeholders with plain paths
  3. Add script module tag: In index.html, add <script type="module" src="/src/main.tsx"></script>
  4. Update env vars: Replace process.env.REACT_APP_* with import.meta.env.VITE_*
  5. Create vite.config.ts: Add your framework plugin and configure any aliases or proxies you had in Webpack
  6. Update package.json scripts: Replace react-scripts start/build with vite/vite build
  7. Handle CommonJS modules: Vite expects ESM. If dependencies use CJS, they'll be auto-converted by esbuild's pre-bundler — but some edge cases need manual handling via optimizeDeps
  8. Remove Webpack config and loaders: Webpack-specific files (webpack.config.js, babel configs, etc.) can be deleted once the migration is confirmed working

Key Takeaways

Vite has earned its position as the default build tool recommendation for most new JavaScript projects — and for good reason. Here's what makes it worth adopting:

Why Vite Wins the Developer Experience Battle

  • Instant dev server: No bundling means cold start in milliseconds, regardless of project size
  • Precise HMR: Only the changed module is invalidated — your state survives edits
  • Zero-config TypeScript: TS, JSX, CSS Modules, JSON, and Web Workers work out of the box
  • Rollup production builds: Excellent tree-shaking, automatic code splitting, and asset fingerprinting
  • Framework-agnostic: One tool for React, Vue, Svelte, Lit, and vanilla JS projects
  • Small config surface: Sensible defaults mean most projects need fewer than 20 lines of config

The limitations worth knowing: Vite's dev/prod split (esbuild dev, Rollup prod) can occasionally reveal behaviour differences between environments. For micro-frontend architectures with Module Federation, Webpack 5 still has a more mature solution. And if you're migrating a large CRA or Webpack project, expect some friction with CommonJS-only dependencies and process.env references.

"Vite represents the natural evolution of frontend tooling — it stops fighting the browser and starts working with it."
— Evan You, Creator of Vite and Vue.js

For new projects, Vite is the clear default. The developer experience improvement over Webpack and CRA is substantial enough that even teams with established Webpack setups are migrating. Start with npm create vite@latest and experience for yourself what a build tool feels like when it doesn't make you wait.

Vite JavaScript Build Tools Frontend ES Modules HMR esbuild
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.