Vite Deep Dive: Why It Is So Fast

Deeply understand Vite core principles and master the essence of modern frontend build tools

Vite Deep Dive: Why It Is So Fast

Vite (French for “fast”) isn’t just another build tool—it represents a paradigm shift in frontend tooling. Born from the Vue ecosystem and now the preferred choice for React, Svelte, Astro, and more, Vite is redefining the development experience. This article takes you deep into Vite’s secrets of speed.

Pain Points of Traditional Build Tools

The Webpack Era Problems

Traditional bundling flow:
┌─────────────────────────────────────────────────────┐
│                Start dev server                      │
│                       ↓                              │
│  ┌─────────────────────────────────────────────┐    │
│  │         Bundle the entire app                │    │
│  │  entry.js → parse deps → transform → bundle  │    │
│  │                                              │    │
│  │  Even a single line change might require     │    │
│  │  rebundling many modules                     │    │
│  └─────────────────────────────────────────────┘    │
│                       ↓                              │
│                Server ready (30s+)                   │
└─────────────────────────────────────────────────────┘

Problems:

  • Slow startup: Must bundle entire app before starting
  • Slow HMR: Changes require rebuilding affected modules
  • Gets worse with scale: Larger projects mean longer waits

Vite’s Core Innovations

1. Native ESM Development

Vite leverages browser-native ES Modules, skipping the bundling step:

Vite dev mode:
┌─────────────────────────────────────────────────────┐
│                Start dev server                      │
│                       ↓                              │
│  ┌─────────────────────────────────────────────┐    │
│  │      No bundling! Start server directly      │    │
│  │      Pre-bundle deps (third-party only)      │    │
│  └─────────────────────────────────────────────┘    │
│                       ↓                              │
│                Server ready (<1s)                    │
│                       ↓                              │
│  ┌─────────────────────────────────────────────┐    │
│  │  When browser requests module, compile       │    │
│  │  on-demand and serve                         │    │
│  │  GET /src/App.vue → compile → return ESM    │    │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘
<!-- Browser loads ES modules directly -->
<script type="module" src="/src/main.js"></script>
// main.js - Browser natively understands these imports
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

2. Dependency Pre-Bundling (esbuild)

Vite uses esbuild (written in Go) to pre-bundle third-party dependencies:

// vite.config.js
export default {
  optimizeDeps: {
    include: ['lodash-es', 'axios'],  // Force pre-bundle
    exclude: ['your-local-package'],   // Exclude from pre-bundling
  }
}

Why pre-bundle?

  1. CommonJS to ESM: Many npm packages still use CommonJS
  2. Merge small modules: lodash-es has 600+ modules, pre-bundled into one
  3. Cache optimization: Results cached in node_modules/.vite
esbuild vs other tools:
┌────────────────────────────────────────┐
│ Tool        │ Bundling three.js time   │
├────────────────────────────────────────┤
│ esbuild     │ 0.37s                    │
│ Rollup      │ 32.07s                   │
│ Webpack 5   │ 41.53s                   │
│ Parcel 2    │ 56.71s                   │
└────────────────────────────────────────┘
         esbuild is 100x faster!

3. Lightning-Fast HMR

// Traditional HMR: rebuild entire module graph
moduleA.js changed
  → rebuild moduleA
  → check modules depending on moduleA
  → rebuild those modules
...recursive...
  → send updates to browser

// Vite HMR: precise updates
moduleA.js changed
  → only invalidate moduleA
  → browser re-requests moduleA
  → server compiles and returns instantly
// Custom HMR handling
if (import.meta.hot) {
  import.meta.hot.accept('./module.js', (newModule) => {
    // Handle module update
    console.log('Module updated:', newModule);
  });

  // Cleanup side effects
  import.meta.hot.dispose(() => {
    cleanup();
  });
}

Core Configuration Explained

Basic Configuration

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  // Plugins
  plugins: [vue()],

  // Path aliases
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
    },
  },

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

  // Build configuration
  build: {
    target: 'es2020',
    outDir: 'dist',
    sourcemap: true,
    minify: 'esbuild',  // or 'terser'
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router'],
          utils: ['lodash-es', 'dayjs'],
        },
      },
    },
  },

  // CSS configuration
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`,
      },
    },
    modules: {
      localsConvention: 'camelCase',
    },
  },
})

Environment Variables

# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
// Use in code
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.MODE);  // 'development' or 'production'
console.log(import.meta.env.DEV);   // true or false
console.log(import.meta.env.PROD);  // true or false
// env.d.ts - Type declarations
/// <reference types="vite/client" />

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

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Plugin System

Vite plugins are compatible with Rollup plugin interface while extending Vite-specific hooks.

Plugin Structure

// my-vite-plugin.js
export default function myPlugin(options = {}) {
  return {
    name: 'my-plugin',

    // Vite-specific hooks
    configResolved(config) {
      // After config is resolved
      console.log('Final config:', config);
    },

    configureServer(server) {
      // Configure dev server
      server.middlewares.use((req, res, next) => {
        // Custom middleware
        next();
      });
    },

    transformIndexHtml(html) {
      // Transform HTML
      return html.replace(
        '</head>',
        `<script>console.log('Injected!')</script></head>`
      );
    },

    // Rollup-compatible hooks
    transform(code, id) {
      if (id.endsWith('.custom')) {
        return {
          code: transformCustomFormat(code),
          map: null,
        };
      }
    },

    // Build hooks
    buildStart() {
      console.log('Build started');
    },

    buildEnd() {
      console.log('Build finished');
    },
  };
}

Common Official Plugins

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

export default defineConfig({
  plugins: [
    // Vue support
    vue(),

    // React support (includes Fast Refresh)
    react(),

    // Legacy browser compatibility
    legacy({
      targets: ['defaults', 'not IE 11'],
    }),
  ],
})

Advanced Features

1. Glob Imports

// Sync import: statically analyzed at build time
const modules = import.meta.glob('./modules/*.js', { eager: true });
/*
{
  './modules/a.js': { default: ... },
  './modules/b.js': { default: ... },
}
*/

// Async import: lazy loading
const lazyModules = import.meta.glob('./modules/*.js');
/*
{
  './modules/a.js': () => import('./modules/a.js'),
  './modules/b.js': () => import('./modules/b.js'),
}
*/

// Usage
for (const path in lazyModules) {
  const module = await lazyModules[path]();
  module.default.init();
}

2. Web Workers

// Inline worker
import MyWorker from './worker?worker&inline'
const worker = new MyWorker()

// Shared worker
import SharedWorker from './shared-worker?sharedworker'

// Workers can use imports
// worker.js
import { heavyComputation } from './utils'

self.onmessage = (e) => {
  const result = heavyComputation(e.data)
  self.postMessage(result)
}

3. WebAssembly

// Import wasm
import init, { greet } from './hello.wasm?init'

const instance = await init()
greet('World')

// Or get raw URL
import wasmUrl from './hello.wasm?url'
const response = await fetch(wasmUrl)

4. SSR Support

// vite.config.js
export default defineConfig({
  build: {
    ssr: true,
  },
  ssr: {
    // These deps won't be externalized
    noExternal: ['vue', '@vueuse/core'],
    // These deps will be externalized
    external: ['fsevents'],
  },
})
// server.js
import { createServer } from 'vite'
import express from 'express'

const app = express()

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

app.use(vite.middlewares)

app.use('*', async (req, res) => {
  const url = req.originalUrl
  const template = await vite.transformIndexHtml(url, html)
  const { render } = await vite.ssrLoadModule('/src/entry-server.js')
  const appHtml = await render(url)

  const finalHtml = template.replace('<!--app-html-->', appHtml)
  res.send(finalHtml)
})

Performance Optimization Tips

1. Code Splitting Strategy

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // Third-party libs in separate chunks
          if (id.includes('node_modules')) {
            // Large libs get their own chunk
            if (id.includes('lodash')) return 'lodash'
            if (id.includes('chart.js')) return 'charts'
            // Other vendors
            return 'vendor'
          }
        },
      },
    },
  },
})

2. Lazy Loading

// Route-level lazy loading
const routes = [
  {
    path: '/',
    component: () => import('./pages/Home.vue'),
  },
  {
    path: '/about',
    component: () => import('./pages/About.vue'),
  },
]

// Conditional loading
const loadHeavyFeature = async () => {
  const { HeavyChart } = await import('./HeavyChart')
  return HeavyChart
}

3. Preloading

<!-- Vite auto-generates preload directives -->
<link rel="modulepreload" href="/assets/vendor.js">
// Manual preloading
import.meta.glob('./pages/*.vue', {
  eager: false,
  import: 'default',
})

Comparison with Other Tools

FeatureViteWebpackParcel
Cold start~300ms10-60s5-30s
HMR<50ms1-10s1-5s
Production buildRollupWebpackParcel
Config complexityLowHighVery Low
Plugin ecosystemRollup compatibleRichestLimited
Native ESM

Migration Guide

From Create React App

# 1. Install dependencies
npm install vite @vitejs/plugin-react -D

# 2. Create vite.config.js
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})
// package.json
{
  "scripts": {
-   "start": "react-scripts start",
-   "build": "react-scripts build",
+   "dev": "vite",
+   "build": "vite build",
+   "preview": "vite preview"
  }
}
<!-- Move index.html to root -->
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- Add entry script -->
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Summary

Why Vite is fast comes down to:

TechnologyPurpose
Native ESMSkip bundling, compile on-demand
esbuildUltra-fast dependency pre-bundling
Precise HMROnly update changed modules
RollupProduction build optimization

Key Takeaways:

  1. No bundling in dev—leverage browser-native ESM
  2. esbuild pre-bundling makes dependency loading blazing fast
  3. HMR updates are precise to module level
  4. Plugin system is compatible with Rollup ecosystem
  5. Production builds still use mature Rollup

Vite isn’t just “faster Webpack”—it represents a new development philosophy: embrace browser-native capabilities, leave complexity to build time, return speed to developers.


Better DX isn’t a luxury—it’s a productivity guarantee. Choose Vite, and make waiting a thing of the past.