Writing Data note

Web Development · 4 min read

I Would Start with Vite and Make Webpack Prove Itself

The benchmark gap was obvious, but deleting 90 lines of build configuration was the better reason to choose Vite for a modern app.

In this article5 sections

The benchmark gap between Vite and Webpack was obvious. The better migration result was deleting build configuration I no longer had to understand or maintain.

The Speed Gap

MetricViteWebpackVite’s Advantage
Dev server startup376ms6s15.9x faster
Hot module reloadNo noticeable wait1.5sRatio not measured
Production build2s11s5.5x faster
Bundle size866KB934KB7.3% smaller

These are the figures recorded for a medium-sized Vue project. The archive does not include versions, hardware, repeated samples, or a benchmark harness, so I cannot offer them as a reproducible comparison. A React comparison showed a similar direction:

MetricViteWebpackVite’s Advantage
Speed370ms7s18.9x faster
Changes updateNo noticeable wait1.3sRatio not measured
Production build4s11s2.8x faster
Bundle size740KB920KB19.6% smaller

For developers making frequent code changes, these speed differences compound across a workday.

Configuration: 110 Lines vs 18

The most obvious difference is config complexity.

Webpack Configuration Example:

// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "[name].[contenthash].js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
    publicPath: "/",
  },
  devtool: "inline-source-map",
  devServer: {
    static: "./dist",
    hot: true,
  },
  optimization: {
    runtimeChunk: "single",
    splitChunks: {
      chunks: "all",
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/,
            )[1];
            return `npm.${packageName.replace("@", "")}`;
          },
        },
      },
    },
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"],
          },
        },
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: "asset/resource",
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Development",
      template: "./public/index.html",
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css",
    }),
  ],
};

Vite Configuration Example:

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    open: true,
  },
  build: {
    outDir: "dist",
    cssCodeSplit: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ["react", "react-dom"],
        },
      },
    },
  },
});

Webpack requires familiarity with loaders, plugins, and optimization settings. Vite ships sensible defaults and gets out of the way.

Inventory the old configuration When migrating, delete your Webpack config entirely and start with a fresh vite.config.js. Trying to port Webpack options one-by-one leads to unnecessary complexity because Vite handles most of them by default.

Hot Module Replacement

In the measured Vite project, changes appeared without a noticeable wait or loss of state:

// src/Hello.jsx
import { useState } from "react";

export default function Hello() {
  const [name, setName] = useState("World");

  return (
    <div>
      <h1>Hello, {name}!</h1>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter a name"
      />
      <p>Try typing your name above</p>
    </div>
  );
}

Changing this component preserved state in Vite. In the Webpack comparison, the update took 1–1.5 seconds.

What changed the development loop

In the 2025 Vite setup, the development server served source modules on demand and used esbuild for dependency pre-bundling; production builds used Rollup. That made the startup path different from the Webpack configuration shown here. It does not establish that JavaScriptCore, native ESM, or any one implementation detail guarantees a particular speedup.

Vite’s documentation describes the current toolchain. The old timings belong to the old configurations, not every Vite or Webpack project.

When Webpack Still Makes Sense

Keep compatibility requirements Despite Vite’s advantages, Webpack remains a strong choice in certain scenarios:

  • IE11 support: Webpack’s ecosystem has more compatibility options for older browsers.
  • Module Federation: Webpack 5’s micro-frontend support is more mature than Vite alternatives.
  • Specialized plugins: Projects relying on Webpack-specific plugins without Vite equivalents are stuck.

Legacy browser support might look like this:

// webpack.config.js for legacy browser support
module.exports = {
  // ...other settings
  target: ["web", "es5"],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  targets: { ie: "11" },
                  useBuiltIns: "usage",
                  corejs: 3,
                },
              ],
            ],
          },
        },
      },
    ],
  },
};

For a new project targeting modern browsers, I start with Vite. I would reach for Webpack only when a concrete requirement, such as mature Module Federation, an irreplaceable plugin, or legacy-browser support, earns the extra configuration.

That is the same threshold I used when I later moved this blog’s generator from Node to Bun: measure the loop, delete configuration only when the replacement earns it, and keep the old tool when a real constraint still depends on it.

Was this useful?

What was missing?

Thanks for the feedback.