Vite’s magic comes from its native ES module support, which means it skips the entire bundling step during development, making builds dramatically faster.

Let’s see this in action. Imagine a simple React app.

Current Webpack Dev Server:

npm run dev
# ... (Webpack starts, bundles everything, serves on localhost:3000)
# Page loads, initial bundle size: 1.2MB
# After 30 seconds, you save a file.
# Webpack recompiles, rebuilds the bundle.
# Page reloads, initial bundle size: 1.2MB

Vite Dev Server:

npm run dev
# ... (Vite starts, serves index.html, serves modules on demand)
# Page loads, initial modules: 50KB (app.jsx), 20KB (react.js), etc.
# After 30 seconds, you save a file.
# Vite serves only the updated module (e.g., 2KB for app.jsx).
# Browser immediately updates via HMR.

The core problem Vite solves is the combinatorial explosion of module dependencies in large JavaScript projects. Webpack, by default, bundles everything into a few large files for production. While this is great for browser caching and performance in production, it makes the development server incredibly slow. Every change requires Webpack to re-bundle, and with hundreds or thousands of modules, this process grinds to a halt.

Vite flips this on its head. For development, it leverages the browser’s native support for ES Modules (import/export). When you start the Vite dev server, it doesn’t bundle your code. Instead, it serves your application as a collection of native ES modules. When the browser requests a module, Vite serves it. If you change a file, Vite only needs to invalidate that specific module and its dependents, pushing the update to the browser almost instantaneously via Hot Module Replacement (HMR).

For production builds, Vite uses Rollup under the hood. Rollup is also a module bundler, but it’s optimized for libraries and single-page applications and generally produces more efficient output than Webpack. Vite configures Rollup with sensible defaults for modern applications, including pre-bundling dependencies using esbuild (which is orders of magnitude faster than JavaScript-based bundlers) and optimizing code splitting.

Here’s how you’d migrate a typical React project:

  1. Install Vite and necessary plugins: If you’re using React, you’ll need the React plugin.

    npm install --save-dev vite @vitejs/plugin-react
    # or
    yarn add --dev vite @vitejs/plugin-react
    
  2. Create a vite.config.js file: This file replaces your webpack.config.js.

    // vite.config.js
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    
    export default defineConfig({
      plugins: [react()],
    });
    
  3. Update package.json scripts: Replace your Webpack dev and build scripts.

    {
      "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview"
      }
    }
    
  4. Adjust your index.html: Vite uses index.html as its entry point. Move your index.html to the root of your project (if it’s not already there) and ensure your script tags are set to type="module".

    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <link rel="icon" type="image/svg+xml" href="/vite.svg" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Vite + React</title>
    </head>
    <body>
      <div id="root"></div>
      <script type="module" src="/src/main.jsx"></script>
    </body>
    </html>
    

    Note that Vite automatically handles the /src/main.jsx import. You don’t need to specify a full path if it’s in the src directory.

  5. Handle Environment Variables: Vite uses import.meta.env. Variables prefixed with VITE_ are exposed to your client-side code.

    // .env
    VITE_API_URL=https://api.example.com
    
    // src/App.jsx
    console.log(import.meta.env.VITE_API_URL);
    

    Webpack typically uses process.env.NODE_ENV and custom variables through DefinePlugin. Vite handles NODE_ENV automatically and exposes VITE_ prefixed variables.

  6. Configure Aliases (Optional): If you used Webpack aliases (e.g., @ for src), you can configure them in vite.config.js:

    // vite.config.js
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    import path from 'path';
    
    export default defineConfig({
      plugins: [react()],
      resolve: {
        alias: {
          '@': path.resolve(__dirname, './src'),
        },
      },
    });
    

When you run vite build, Vite leverages Rollup and esbuild for dependency pre-bundling. esbuild is a blazingly fast Go-based bundler that Vite uses to quickly convert CommonJS or UMD dependencies into native ES modules. This pre-bundling step is crucial because native ES modules have dynamic import behavior, which can lead to waterfall requests if not handled. By pre-bundling, Vite creates optimized, static ES modules for your dependencies, ensuring efficient loading and caching in production.

The most common stumbling block during migration is understanding how Vite handles static assets like images and CSS. Unlike Webpack, which often requires specific loaders or plugins to process these, Vite treats them as standard ES modules. When you import an asset, Vite returns its public URL. For CSS, you can import .css files directly into your JavaScript, and Vite will handle injecting them into the DOM.

After migrating, you’ll likely encounter issues with specific Webpack loaders that don’t have direct Vite equivalents. For instance, if you were using a custom Webpack loader for a niche file type, you’d need to find a Vite plugin that performs the same transformation or write your own Vite plugin.

The next concept you’ll explore is optimizing Vite’s production builds further, especially for very large applications, by fine-tuning its Rollup configuration.

Want structured learning?

Take the full Vite course →