Vite is the bundler that pretends it’s not a bundler, at least for development.

Let’s see Vite in action. Imagine you’re building a React app. Your package.json might look like this:

{
  "name": "my-react-app",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.0.0",
    "vite": "^4.4.5"
  }
}

When you run npm run dev, Vite starts up a dev server. Instead of bundling your entire application upfront, it leverages native ES modules in the browser. This means when your browser requests src/main.jsx, Vite serves that file directly. If main.jsx imports App.jsx, the browser requests App.jsx, and so on. This creates a dependency graph on the fly.

$ npm run dev
vite v4.4.5  ready in 300ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Notice how fast that startup is, even for a moderately sized project. That’s the magic of native ESM.

Now, let’s look at esbuild. esbuild is a bundler and minifier written in Go. Its primary strength is its incredible speed, achieved through parallelization and its compiled nature. It’s often used within other tools like Vite for certain tasks, but it can also be used directly.

Here’s a simple esbuild command to bundle a JavaScript file:

$ npx esbuild src/index.js --bundle --outfile=dist/bundle.js --platform=browser --format=esm

This command takes src/index.js, bundles all its dependencies, outputs a single bundle.js file in the dist directory, targets the browser, and produces an ES module. The speed at which esbuild performs this is astonishing, often measured in milliseconds for even large projects.

The core problem both esbuild and Vite solve is transforming modern JavaScript (and other assets like CSS, images, etc.) into a format that browsers can understand and execute efficiently, especially for production.

Vite’s Mental Model:

  • Development: Leverages native ES Modules. No upfront bundling. Server-Sent Events (SSE) are used for Hot Module Replacement (HMR). When you change a file, only the affected module and its direct dependencies are re-processed and sent to the browser. This makes edits feel instantaneous.
  • Production: Uses Rollup under the hood for optimized production builds. This means it does bundle your code for performance, generating highly optimized chunks, code-splitting, and tree-shaking.

esbuild’s Mental Model:

  • Speed First: esbuild’s primary design goal is speed. It’s a compiler and bundler that can be used for various tasks: bundling, minifying, transpiling.
  • Pluggable: While fast on its own, its power is amplified when integrated into other tools. Vite uses esbuild for its initial dependency pre-bundling (converting CommonJS to ESM) and for minification in production builds.

When does esbuild win?

esbuild shines when you need raw speed for bundling or minification, and you don’t need the full dev server experience that Vite provides. This often means:

  1. CI/CD Pipelines: If your build process in CI/CD needs to quickly bundle or minify code without a dev server, esbuild is your go-to.
    # Example: Minify a JS file
    echo "const x = 1; console.log(x + 2);" > input.js
    npx esbuild input.js --minify --outfile=output.js
    cat output.js
    # Output: var x=1;console.log(x+2);
    
  2. Custom Tooling: If you’re building your own compiler, linter, or other build tools that require fast JS processing, esbuild is a fantastic foundational piece.
  3. As a Dependency: As mentioned, Vite uses esbuild. If you’re building a framework or tool that needs a fast bundler, you might incorporate esbuild directly.

When does Vite win?

Vite wins overwhelmingly for modern frontend development workflows.

  1. Developer Experience (DX): The near-instantaneous dev server startup and HMR are game-changers. No more waiting minutes for your app to reload after a change.
    # After running `npm run dev`, make a change in src/App.jsx
    # Observe the browser automatically updating with no manual refresh needed.
    
  2. Framework Agnostic: Vite has first-class support for React, Vue, Svelte, Preact, and vanilla JS. Its plugin system is robust and easy to use.
  3. Production Optimization: While its dev server is fast due to native ESM, its production builds leverage Rollup, ensuring highly optimized, small, and fast-loading bundles.
    // vite.config.js example for React
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    
    export default defineConfig({
      plugins: [react()],
      build: {
        rollupOptions: {
          output: {
            manualChunks(id) {
              if (id.includes('node_modules')) {
                return id.toString().split('node_modules/')[1].split('/')[0].toString();
              }
            }
          }
        }
      }
    })
    
    This vite.config.js shows how you can configure Rollup (used by Vite for builds) to split vendor dependencies into their own chunks, improving caching.

A subtle point often missed is how Vite handles dependency pre-bundling. When you first start vite dev, it scans your import statements and uses esbuild to pre-bundle your dependencies into separate modules. This is crucial because node_modules often contain CommonJS modules or older JS formats that browsers don’t natively understand. Vite converts these into ESM on the fly using esbuild, making them usable by the browser’s native import system and significantly speeding up the initial server response.

The biggest differentiator is the development experience. Vite’s approach to leveraging native ESM for instant server start and HMR is its killer feature, making esbuild, while incredibly fast, a less complete solution for the entire development lifecycle.

The next logical step after understanding these bundlers is exploring how they interact with different JavaScript dialects, like TypeScript.

Want structured learning?

Take the full Esbuild course →