Migrating from Webpack 5 to Vite
You have a working Webpack 5 application and want Vite’s native-ESM dev server and Rollup production build without a multi-week rewrite. This guide maps each Webpack concept to its Vite equivalent and lists the breakages to expect; because most ports lean on a custom plugin or two, keep Advanced Vite Plugin Configuration open for the loader-to-plugin cases. The mental shift is that Vite serves source over native ESM in dev (no bundling) and only bundles for production, so anything that assumed a CommonJS webpack runtime needs attention.
Prerequisites & Reproducible Setup
# Vite 5.x / 6.x, Node 20+
npm install -D vite @vitejs/plugin-react
# Keep the old build working until parity is reached:
# npm run build:webpack -> webpack --mode production
# npm run dev -> vite
Add Vite alongside Webpack rather than ripping Webpack out first. Run both until the Vite build reaches parity, then delete webpack.config.js and the Webpack dependencies in one commit.
Step 1: index.html Becomes the Entry Point
Webpack starts from a JS entry and injects script tags via HtmlWebpackPlugin. Vite inverts this: index.html lives at the project root and is the entry, referencing your source with a normal <script type="module">.
<!-- index.html at project root — Vite 5.x / 6.x -->
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><title>App</title></head>
<body>
<div id="root"></div>
<!-- replaces HtmlWebpackPlugin's injected bundle tag -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Move any template HTML from HtmlWebpackPlugin here. Webpack’s %PUBLIC_URL% and EJS interpolation are gone; Vite uses %VITE_FOO% env replacement and /-rooted public/ assets instead.
Step 2: Loaders Become Built-in Transforms or Plugins
Most loaders disappear because Vite handles them natively. The rest map to a plugin.
| Webpack loader/rule | Vite equivalent |
|---|---|
babel-loader / ts-loader (JS/TS/JSX) |
Built-in esbuild transform (add @vitejs/plugin-react for React Fast Refresh) |
css-loader + style-loader |
Built-in .css import |
sass-loader |
Built-in once sass is installed |
file-loader / url-loader |
Built-in asset handling (import url from './x.png') |
raw-loader |
?raw import suffix |
svgr |
vite-plugin-svgr |
| custom loader | A custom plugin — see the asset-transform guide |
For a bespoke loader, port it to a plugin transform hook as described in Writing a Custom Vite Plugin for Asset Transformation, and mind the enforce lane so it runs at the right time (see Debugging Vite Plugin Hook Order with enforce and apply).
Step 3: require to ESM
Vite serves native ESM, so top-level require() and module.exports break with require is not defined. Convert source to import/export. CommonJS dependencies in node_modules are fine — Vite’s esbuild pre-bundling converts them — but your own source must be ESM. require.context() has no direct equivalent; replace it with import.meta.glob:
// Webpack: const ctx = require.context('./pages', true, /\.tsx$/)
// Vite 5.x / 6.x — eager glob import
const pages = import.meta.glob('./pages/*.tsx', { eager: true });
Step 4: process.env to import.meta.env, DefinePlugin to define
Webpack injects env via DefinePlugin and process.env. Vite exposes only VITE_-prefixed vars on import.meta.env; everything else must move to define or a VITE_ rename.
// vite.config.ts — Vite 5.x / 6.x
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
define: {
// DefinePlugin({ __APP_VERSION__: JSON.stringify('1.4.0') }) becomes:
__APP_VERSION__: JSON.stringify('1.4.0'),
// Shim libraries that still read process.env.NODE_ENV at runtime:
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
},
server: {
// Webpack devServer.proxy becomes server.proxy
proxy: {
'/api': { target: 'http://localhost:3001', changeOrigin: true },
},
},
build: {
outDir: 'dist', // mirror Webpack output.path
sourcemap: true,
rollupOptions: {
// Webpack's optimization.splitChunks cacheGroups -> manualChunks
output: { manualChunks: { vendor: ['react', 'react-dom'] } },
},
},
});
In source, rename process.env.API_URL to import.meta.env.VITE_API_URL and move the value into .env:
# .env — only VITE_-prefixed vars reach client code
VITE_API_URL=https://api.example.com
For the full env-mode model — .env.production, loadEnv, and why bare process.env reads return undefined in the client — see Environment Variables and Build Modes in Vite.
Step 5: Dev-Server Proxy and Static Assets
The proxy block above replaces devServer.proxy; the option names (target, changeOrigin, rewrite) come from the same http-proxy library, so most configs port verbatim. Webpack’s static/contentBase directory becomes Vite’s public/ folder, served at / and copied to dist/ untouched.
Verification
- Dev parity:
viteboots and the app renders withoutrequire is not defined,process is not defined, orFailed to resolve importin the console. - Env values:
console.log(import.meta.env.VITE_API_URL)prints the value; bareprocess.env.API_URLis nowundefined(expected). - Build parity:
vite buildproducesdist/with hashed chunks; diff the asset list against the Webpack output and confirmvendorchunking matches yourmanualChunks. - Preview:
vite previewservesdist/so you can smoke-test the production bundle before swapping CI.
# Vite 5.x / 6.x, Node 20+
vite build && vite preview --port 4173
Gotchas & Edge Cases
process is not definedin the browser. A dependency readsprocess.envat runtime. Add the'process.env.NODE_ENV'shim shown above; for libraries needing the whole object,define: { 'process.env': {} }as a last resort.__dirname/__filenamein client code. These are Node-only; Webpack polyfilled them, Vite does not. Replace withimport.meta.urlor move the logic to the server.- Node core modules imported in the browser. Webpack 4 auto-polyfilled
crypto,buffer, etc.; Webpack 5 dropped that, and Vite never had it. Install browser shims explicitly or remove the import. - CommonJS-only dependency fails to optimize. If a package errors during pre-bundling, list it in
optimizeDeps.includeto force esbuild conversion, orbuild.commonjsOptions.includefor the build pass. - Aliases. Move Webpack
resolve.aliastoresolve.aliasinvite.config.ts— same shape, but Vite expects absolute paths (usefileURLToPath(new URL('./src', import.meta.url))).
Related
- Advanced Vite Plugin Configuration — porting custom loaders to plugin hooks with the right
enforcelane. - Environment Variables and Build Modes in Vite — the full
import.meta.env,.envmode, andloadEnvmodel behind Step 4. - Writing a Custom Vite Plugin for Asset Transformation — replacing a bespoke Webpack loader with a Vite plugin.