Webpack vs Vite vs Rollup: JS Bundler Guide

Compare Webpack, Vite, and Rollup for JavaScript bundling. Covers architecture differences, dev server performance, build output optimization, plugin ecosystems, configuration complexity, migration strategies, and choosing the right bundler for your project.

JavaScriptadvanced
16 min read

Webpack, Vite, and Rollup solve the same problem differently. This guide compares their architecture, performance, configuration, and ideal use cases with practical examples.

For bundler internals, see JavaScript Bundlers: An Advanced Architecture.

Architecture Comparison

javascriptjavascript
// WEBPACK: Bundle-first architecture
// Bundles everything before serving, even in development
// webpack.config.js
module.exports = {
  entry: "./src/index.js",
  output: { filename: "bundle.js", path: __dirname + "/dist" },
  module: {
    rules: [
      { test: /\.jsx?$/, use: "babel-loader" },
      { test: /\.css$/, use: ["style-loader", "css-loader"] },
    ],
  },
  devServer: {
    hot: true,
    port: 3000,
  },
};
 
// VITE: Native ESM in dev, Rollup for production
// vite.config.js
import { defineConfig } from "vite";
 
export default defineConfig({
  server: { port: 3000 },
  build: {
    rollupOptions: {
      output: { manualChunks: { vendor: ["react", "react-dom"] } },
    },
  },
});
 
// ROLLUP: Library-focused, best tree shaking
// rollup.config.js
export default {
  input: "src/index.js",
  output: [
    { file: "dist/bundle.cjs.js", format: "cjs" },
    { file: "dist/bundle.esm.js", format: "es" },
    { file: "dist/bundle.umd.js", format: "umd", name: "MyLib" },
  ],
  plugins: [resolve(), commonjs(), terser()],
};
FeatureWebpackViteRollup
Dev server approachBundles all, serves bundleNative ESM, transforms on demandNo built-in dev server
Cold start (large app)10-30s0.5-2sN/A
HMR speed1-5sinstant (<100ms)N/A
Production bundlerWebpack itselfRollup (under the hood)Rollup
Tree shakingGoodExcellent (Rollup)Best
Code splittingExcellentGoodGood
Plugin ecosystemLargest (2000+)Growing (500+)Moderate (300+)
Config complexityHighLowMedium
Asset handlingLoaders for everythingBuilt-in (CSS, JSON, assets)Plugins needed
Multiple output formatsLimitedESM + legacyCJS, ESM, UMD, IIFE

Development Server Performance

javascriptjavascript
// WHY VITE IS FASTER IN DEVELOPMENT
 
// Webpack (traditional approach):
// 1. Read ALL source files
// 2. Build complete dependency graph
// 3. Transform ALL modules (Babel, TypeScript, etc.)
// 4. Bundle into single file
// 5. Serve bundle to browser
// Time: O(total_modules) on every startup
 
// Vite (ESM approach):
// 1. Start server immediately
// 2. Browser requests index.html
// 3. Browser parses <script type="module" src="/src/main.ts">
// 4. Browser requests /src/main.ts
// 5. Vite transforms ONLY main.ts (using esbuild, 10-100x faster than Babel)
// 6. Browser follows imports, requests only what's needed
// Time: O(route_modules) - only modules for current page
 
// HMR comparison
// Webpack HMR: rebuild affected chunk -> send full chunk -> replace
// Vite HMR: transform single file -> send module update -> hot swap
// Vite HMR is independent of app size
 
// Practical benchmark (1000 module app):
// Webpack cold start: ~18 seconds
// Vite cold start: ~1.2 seconds (15x faster)
// Webpack HMR: ~2.5 seconds
// Vite HMR: ~50ms (50x faster)

Webpack Advanced Configuration

javascriptjavascript
// webpack.config.js - production optimized
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 
module.exports = {
  mode: "production",
  entry: {
    app: "./src/index.js",
    vendor: ["react", "react-dom"],
  },
 
  output: {
    filename: "[name].[contenthash:8].js",
    chunkFilename: "[name].[contenthash:8].chunk.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
 
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: { drop_console: true, passes: 2 },
          mangle: { safari10: true },
        },
        parallel: true,
      }),
      new CssMinimizerPlugin(),
    ],
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        framework: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: "framework",
          priority: 20,
        },
        commons: {
          minChunks: 2,
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },
    runtimeChunk: "single",
  },
 
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              ["@babel/preset-env", { modules: false, useBuiltIns: "usage", corejs: 3 }],
              "@babel/preset-react",
            ],
            cacheDirectory: true,
          },
        },
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
      },
    ],
  },
 
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash:8].css",
    }),
  ],
};

Vite Advanced Configuration

javascriptjavascript
// vite.config.js - production optimized
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { visualizer } from "rollup-plugin-visualizer";
 
export default defineConfig(({ mode }) => ({
  plugins: [
    react(),
    mode === "analyze" && visualizer({
      filename: "dist/stats.html",
      open: true,
      gzipSize: true,
    }),
  ].filter(Boolean),
 
  build: {
    target: "es2020",
    minify: "terser",
    sourcemap: true,
    cssMinify: true,
 
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
 
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            if (id.includes("react")) return "framework";
            if (id.includes("lodash")) return "vendor-lodash";
            return "vendor";
          }
        },
        chunkFileNames: "assets/[name]-[hash].js",
        entryFileNames: "assets/[name]-[hash].js",
        assetFileNames: "assets/[name]-[hash].[ext]",
      },
    },
 
    chunkSizeWarningLimit: 500,
  },
 
  server: {
    hmr: { overlay: true },
    proxy: {
      "/api": {
        target: "http://localhost:4000",
        changeOrigin: true,
      },
    },
  },
 
  resolve: {
    alias: {
      "@": "/src",
      "@components": "/src/components",
      "@utils": "/src/utils",
    },
  },
}));

Rollup for Library Authoring

javascriptjavascript
// rollup.config.js - library build
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import terser from "@rollup/plugin-terser";
import { dts } from "rollup-plugin-dts";
import pkg from "./package.json" assert { type: "json" };
 
const external = [
  ...Object.keys(pkg.dependencies || {}),
  ...Object.keys(pkg.peerDependencies || {}),
];
 
export default [
  // Main build
  {
    input: "src/index.ts",
    external,
    output: [
      {
        file: pkg.main,
        format: "cjs",
        sourcemap: true,
        exports: "named",
      },
      {
        file: pkg.module,
        format: "es",
        sourcemap: true,
      },
    ],
    plugins: [
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json" }),
      terser(),
    ],
  },
  // Type declarations
  {
    input: "src/index.ts",
    output: { file: "dist/index.d.ts", format: "es" },
    plugins: [dts()],
  },
];

Migration: Webpack to Vite

javascriptjavascript
// Step 1: Install Vite
// npm install -D vite @vitejs/plugin-react
 
// Step 2: Create vite.config.js (maps webpack concepts)
 
// Webpack loaders -> Vite handles natively or via plugins
// style-loader + css-loader -> Built-in CSS support
// file-loader -> Built-in asset handling
// babel-loader -> esbuild (default) or @vitejs/plugin-react
 
// Webpack DefinePlugin -> Vite define option
// webpack: new webpack.DefinePlugin({ 'process.env.API_URL': JSON.stringify(url) })
// vite:
export default defineConfig({
  define: {
    "process.env.API_URL": JSON.stringify(process.env.API_URL),
  },
});
 
// Step 3: Move index.html to root (Vite requirement)
// Vite uses index.html as entry point, not a webpack HtmlPlugin template
// Add: <script type="module" src="/src/main.jsx"></script>
 
// Step 4: Update environment variables
// webpack: process.env.REACT_APP_*
// vite: import.meta.env.VITE_*
// Find and replace across codebase
 
// Step 5: Update require() to import
// Vite requires ES module syntax
// require("./style.css") -> import "./style.css"
// require("./logo.png") -> import logo from "./logo.png"
 
// Step 6: Handle Node.js globals
// Vite does not polyfill Node.js globals (process, Buffer, etc.)
export default defineConfig({
  define: {
    "process.env": {},
    global: "globalThis",
  },
});
Rune AI

Rune AI

Key Insights

  • Vite is 10-50x faster in development: Native ESM serving and esbuild transforms eliminate the bundling step during development, making cold start and HMR nearly instant
  • Rollup produces the smallest bundles: Superior tree shaking and scope hoisting make Rollup the best choice for library output, and Vite inherits this for production builds
  • Webpack has the largest plugin ecosystem: Over 2000 plugins and loaders handle virtually any use case, making it the safest choice for complex enterprise applications
  • Migration from Webpack to Vite is straightforward: Most webpack concepts map directly to Vite equivalents, with the main changes being index.html placement, environment variable prefixes, and removing Node.js polyfills
  • Choose based on project type: Vite for applications, Rollup for libraries, Webpack for legacy projects or when specific loaders are required
RunePowered by Rune AI

Frequently Asked Questions

Should I migrate from Webpack to Vite?

Migrate if your development server is slow (over 5 seconds cold start or slow HMR). Vite significantly improves developer experience. However, if your webpack config is stable and you have many custom loaders/plugins, the migration cost may not be worth it for mature projects. New projects should start with Vite.

Which bundler produces the smallest output?

Rollup typically produces the smallest output due to its superior tree shaking and scope hoisting. Vite uses Rollup for production builds, so it matches Rollup's output quality. Webpack's output includes slightly more runtime code but the difference is usually under 5% for large applications. For libraries, Rollup is the clear winner.

Can I use Vite without a framework like React or Vue?

Yes. Vite works with vanilla JavaScript, TypeScript, and any framework. Without a framework plugin, Vite still provides fast HMR, CSS handling, asset optimization, and Rollup-based production builds. It is an excellent choice for vanilla JS projects, web components, or custom framework projects.

Why does Rollup not have a dev server?

Rollup is designed as a production bundler focused on generating optimized output. It excels at library authoring where a dev server is less important. For applications needing a dev server, Vite provides Rollup's bundling quality with a fast ESM-based dev server on top. Using Rollup directly for applications requires additional tooling.

How do I handle CSS Modules across bundlers?

Webpack uses `css-loader` with `modules: true`. Vite supports CSS Modules automatically for files named `*.module.css`. Rollup requires `rollup-plugin-postcss` with `modules: true`. The syntax in source code is identical across all three: `import styles from './Component.module.css'`.

Conclusion

Webpack excels at complex applications with extensive plugin needs. Vite provides the best development experience with near-instant HMR. Rollup produces the most optimized output for libraries. For new projects, start with Vite. For bundler internals, see JavaScript Bundlers: An Advanced Architecture. For tree shaking configuration, see JavaScript Tree Shaking: A Complete Tutorial.