Tools

Vite: The Lightning-Fast Build Tool

Mayur Dabhi
Mayur Dabhi
June 13, 2026
14 min read

You run npm start on a big Webpack or Create React App project and go make coffee while it bundles. Then you fix one typo in a component, wait a few seconds for HMR, and the form you'd half filled in resets anyway. That loop gets old fast, and it's the exact problem Vite was built for.

Vite (French for "quick") was created by Evan You, who also wrote Vue.js. This post covers why its dev server is fast, how to set up and configure a project, how production builds work, and the spots where it can still bite you.

Where the Speed Comes From

Vite doesn't bundle your app during development. Dependencies get pre-bundled once with esbuild (written in Go and much faster than JS-based bundlers), and your own source files are served to the browser as native ES modules. Startup usually takes well under a second, and it doesn't grow much as the project gets bigger.

Why Traditional Bundlers Are Slow

Webpack and similar tools have to process your entire application before they can serve a single page. On a large app with thousands of modules, that shows up in three ways:

The root cause is architectural. Bundlers were designed when browsers couldn't load ES modules on their own. That stopped being true years ago: every major browser has supported native ESM since around 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 has an official scaffolding CLI with templates for React, Vue, Svelte, Lit, Preact, and vanilla JS, each with a TypeScript variant.

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

Go into the project folder, install, and start. The dev server comes up almost immediately.

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

The scaffolded project is small. There isn't much boilerplate to delete.

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 and Vite entry (not in 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, which is the same way a browser loads an ESM app.

How the Vite Dev Server Works

Vite splits your modules into two groups and handles each one differently. That split is why it stays fast as the project grows.

Pre-bundled Dependencies (node_modules)

Packages from node_modules are pre-bundled with esbuild the first time you start the server, and again whenever your dependencies change. The output is cached in node_modules/.vite/deps/ and served as single ESM files. Without this step, a package like lodash-es would make the browser fire off hundreds of separate requests.

One thing that trips people up: if you see new dependencies optimized: ... reloading in the terminal and the page does a full reload, Vite found an import it didn't pre-bundle at startup. It's harmless but annoying. Add the package to optimizeDeps.include so it's bundled up front. And if the cache ever gets stuck after switching branches or patching a package, run vite --force (or delete node_modules/.vite) to rebuild it.

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 files the browser actually asks for get processed, so a big module that the current page never imports costs nothing at startup.

Hot Module Replacement (HMR)

When you change a file, Vite invalidates that module and walks up to the nearest HMR boundary. The update the browser receives stays small no matter how many other files the project has. That's why HMR still feels instant in large apps where Webpack can take 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

Configuration lives in vite.config.ts (or vite.config.js). Wrap it in defineConfig and your editor will autocomplete every option.

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'],
    },
})

Dev server settings: 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, so most Rollup plugins work in Vite without changes. These are the ones I'd look at first.

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

Useful 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

A Vite plugin is just an object with hook functions. This one 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

For production, Vite bundles with Rollup, not esbuild. That's on purpose. esbuild is very fast, but its code splitting and chunk control aren't as flexible as Rollup's. You get tree-shaken bundles with CSS split per chunk automatically.

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

A Vite build is just static files. Deploy it 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

Client code reads environment variables through import.meta.env. Only variables prefixed with VITE_ reach the browser, so secrets without the prefix stay out of the bundle. The flip side: anything you do prefix with VITE_ is baked into public JavaScript, so never put an API secret there.

.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

For most new projects I'd pick Vite. Webpack still makes sense in large codebases with custom build pipelines that grew over years, where rewriting that config would cost more than it saves.

Feature Vite 5 Webpack 5
Dev server cold start Usually under a second (no bundling) Seconds to a minute on big apps (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_*. If you miss one, you'll get Uncaught ReferenceError: process is not defined in the browser console, because Vite doesn't polyfill process like CRA did
  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, the pre-bundler converts them automatically, 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

When I'd Use Vite (and When I Wouldn't)

For a new SPA, a Laravel front end, or a component library, I'd reach for Vite without thinking about it much. Laravel ships with it by default now, and the config you write is short enough that a new teammate can read all of it.

Cases where I'd slow down

  • You rely on Module Federation: Webpack 5 supports it natively. The Vite plugins work, but they're less mature, so test the remote loading carefully before committing.
  • Your Webpack config is years of custom loaders: If the build works and nobody complains about dev speed, a migration is a cost with little payoff. Fix the real bottleneck first.
  • You depend on old CommonJS-only packages: Most get converted during pre-bundling, but packages that do dynamic require() calls or touch Node globals can break in odd ways.
  • You need to support very old browsers: It works through @vitejs/plugin-legacy, but you're shipping two sets of bundles and the build gets slower.

The other thing to keep in mind is that dev and production go through different pipelines (native ESM plus esbuild in dev, Rollup in prod). Now and then something works in npm run dev and breaks in the build, usually around CommonJS interop or dynamic imports. So don't treat the dev server as proof. Run npm run build && npm run preview locally, or in CI, before you deploy.

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.