Skip to content

Vite vs Webpack in 2026: The Migration Cost Nobody Quotes You

Vite vs Webpack in 2026: The Migration Cost Nobody Quotes You

Short version for the impatient: if you are starting something new, use Vite and stop reading. If you are maintaining a webpack app that works, the migration is probably not worth it this quarter, and the reason has almost nothing to do with build speed.

I say that as someone who has now done this migration three times. The first one took an afternoon and I felt like a genius. The second one took two weeks and I stopped telling people how it was going. The third one I quoted at a week, and it took a week, which is the only part of this story I am actually proud of.

The gap between the afternoon and the two weeks is the thing nobody puts in the comparison posts. So this is my attempt at that post: not vite vs webpack on benchmark numbers, but on what the move actually costs.

What actually changed in Vite 8

For most of its life Vite ran two bundlers. esbuild handled the fast on-the-fly transforms during development, and Rollup produced the final production bundle. That split worked well enough, and it also produced a long tail of “works in dev, breaks in build” bugs that traced back to exactly that seam. If you have ever had a dependency behave differently after npm run build, you have met it.

Vite 8 collapses that. Per the Vite 8 announcement, Rolldown, a Rust bundler with a Rollup-compatible API, now handles both jobs. Oxc replaces esbuild’s transform step and Lightning CSS becomes the default CSS minifier. The Rolldown integration guide covers what that means for existing plugins.

One engine for dev and build is a bigger deal to me than any speed number. It means the mental model gets smaller. When something behaves oddly, there is one place to look.

The dev server was never the hard part

Here is my honest read after doing this three times: the dev server improvement is real and it is also the part people over-weight in the decision.

Yes, native ESM in dev means the server starts in under a second instead of forty. Yes, hot module replacement stays fast as the app grows because Vite only transforms the module you touched. I am not going to pretend that does not matter. Starting the dev server used to be the moment I checked Slack, and now it is not.

But you restart the dev server what, ten times a day? Fifteen? That is a few minutes back. Nice, not transformative. The thing that ate two weeks on migration number two was not speed. It was a webpack.config.js that had accumulated eleven loaders, four plugins, three custom resolve aliases, and a DefinePlugin block that six people had appended to over four years.

Config translation is the easy half

The simple config maps over almost mechanically. Here is a trimmed webpack setup:

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/main.jsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  resolve: {
    extensions: ['.js', '.jsx'],
    alias: { '@': path.resolve(__dirname, 'src') },
  },
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
      { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] },
      { test: /\.svg$/, use: '@svgr/webpack' },
    ],
  },
  devServer: { port: 3000, historyApiFallback: true },
};

And the Vite equivalent:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import path from 'node:path';

export default defineConfig({
  plugins: [react(), svgr()],
  resolve: {
    alias: { '@': path.resolve(__dirname, 'src') },
  },
  server: { port: 3000 },
});

Look at what disappeared. No entry, because index.html is the entry. No output filename template, because content hashing is the default. No babel-loader, no style-loader, no css-loader, no postcss-loader; CSS and PostCSS are handled if a postcss.config.js exists. No historyApiFallback, because the dev server does that already.

That is the version of this migration that takes an afternoon, and it is the version that shows up in every tutorial. Nine files in your project, all of them modern, no weird build steps.

Where the two weeks actually go

Migration number two had a jQuery plugin loaded via imports-loader, an SCSS setup with three levels of includePaths, a webpack ProvidePlugin shimming a global that four files depended on and nobody had documented, and a chunk of build-time codegen wired in through a custom plugin.

None of that is a Vite problem exactly. It is a “you had four years of accumulated build archaeology and now you have to read all of it” problem. Every one of those took an hour of reading to understand, and the person who wrote it had left.

The rule I use now when quoting this work: count the entries in module.rules and the plugins array. Under six total, quote an afternoon. Six to twelve, quote three days. Over twelve, do not quote a number until you have read every one of them, because at least two will turn out to be load-bearing in a way the file does not admit. I hit the same wall when I moved a project off Jest, and I wrote about that in my post on finally switching test runners. Same shape of problem: the tool swap is trivial, the config archaeology is not.

Where webpack still genuinely wins

I want to be fair here, because “webpack is dead” is lazy and wrong.

Module Federation is the real one. The webpack docs on it describe letting separate builds expose and consume modules from each other at runtime, which is how a lot of large organisations do micro frontends. Vite has community plugins in this space and they are fine for straightforward cases, but if you have four teams shipping independently deployed remotes with shared dependency negotiation, webpack’s implementation is the mature one and you should think hard before leaving it.

The second is the plugin ecosystem’s long tail. Webpack has been the default for a decade, so for any obscure asset type or legacy framework there is a loader that someone battle-tested in 2018. If your app depends on one of those, the Vite equivalent may not exist and you will be writing it.

The third is boring and underrated: it currently works. A build system that produces correct output and that your team understands has value that does not show up in a benchmark. Replacing it consumes a week you could spend on something a customer would notice.

The numbers, and what they do not measure

The Vite team’s own benchmark on a 19,000 module project has a production build going from 40.10 seconds under Rollup to 1.61 seconds under Rolldown. Small codebases see something closer to 2x to 5x, large ones see the bigger multiples. Those are the team’s figures on their own benchmark, so read them as directional rather than as a promise about your repo.

Even taking them at face value, here is the question I ask before treating build time as the reason to migrate: how many times a day does anyone actually wait for a production build?

For most teams the answer is “in CI, on merge, and nobody is watching.” Cutting that from four minutes to one is a real cost saving on runner minutes and a small quality of life win for whoever is waiting on a deploy. It is not usually worth a two week migration on its own. Combine it with a dev server that starts instantly and a config file that a new hire can read in one sitting, and now the case is stronger.

The compatibility layer helps too. Most rollupOptions and esbuild config auto-converts to the Rolldown and Oxc equivalents, so upgrading an existing Vite project is not the same class of work as leaving webpack.

The three things that break on the first build

Assuming you go ahead, here is what has broken on me every single time, in order of how long it took to figure out.

Environment variables. Webpack projects usually pass them in through DefinePlugin or EnvironmentPlugin, and code all over the app reads process.env.SOMETHING. Vite exposes import.meta.env instead and only inlines variables prefixed with VITE_. So every process.env.API_URL in your source needs to become import.meta.env.VITE_API_URL, and the ones you miss do not error, they come out as undefined at runtime and you find them in staging. Grep for process.env before you start and fix all of them in one commit.

Node built-ins in browser code. Webpack 4 silently polyfilled path, buffer and friends. Webpack 5 stopped and told you to add the polyfill yourself. Vite also does not polyfill, and the error message points at the import rather than at the dependency that pulled it in, so you end up walking the dependency tree by hand. This is usually one bad transitive dependency and the fix is either an alias or replacing the package.

CommonJS dependencies that lie about their exports. Vite pre-bundles dependencies in dev, and a package with a malformed main field or a conditional export map will resolve differently than it did under webpack. The symptom is a default import arriving as an object with a default property nested inside it. Annoying, quick to fix once you have seen it, genuinely baffling the first time.

None of these are hard. All three cost me a couple of hours the first time and about ten minutes every time after. Budget half a day for them and you will be fine.

How I would decide this week

Do not read another comparison. Do this instead, it takes about twenty minutes.

Open your webpack.config.js and count two things: the number of rules in module.rules, and the number of entries in plugins. Write both numbers down. Then go through the list and mark every one you cannot explain out loud in a sentence. That count, not the build time, is your migration estimate.

If the unexplainable count is zero, migrate on a Friday afternoon, you will be done before dinner. If it is two or three, put it in the next sprint with a real estimate. If it is eight, the honest answer is that this is a project and not a chore, and it needs to compete with everything else on the roadmap on its merits.

And if you are starting something new, there is no decision to make. Use Vite, keep the config file under thirty lines, and try to make sure the person who inherits it in 2030 does not have to write this post again. Most of the build tooling work I get called in for is exactly this kind of untangling, and the pattern is always the same: the tool was never the problem, the four years of undocumented exceptions were.