Understanding the Migration from Vite to Webpack
Migrating from Vite to Webpack means transitioning your project's build system from Vite's modern, ESM-first bundler to Webpack's mature, highly configurable module bundler. While Vite excels at development speed through native ES modules and lightning-fast HMR, Webpack offers a more established ecosystem with extensive plugin support, finer control over the build pipeline, and broader compatibility with legacy systems. This migration typically becomes necessary when your project requires Webpack-specific features, needs to integrate with older CI/CD pipelines, or must align with organizational standardization around Webpack.
The core differences you'll encounter center around configuration philosophy. Vite relies on native ES modules during development and uses Rollup for production builds, keeping configuration minimal and opinionated. Webpack, conversely, requires explicit configuration for every aspect of module resolution, transformation, and optimization. Understanding this fundamental shift is crucial before beginning the migration.
Why Migrate from Vite to Webpack?
Several practical reasons might drive this migration:
- Enterprise standardization: Many large organizations have standardized build pipelines around Webpack, requiring all projects to use the same toolchain
- Plugin ecosystem: Webpack's plugin ecosystem is more extensive and mature, offering solutions for edge cases that Vite plugins may not cover
- Legacy browser support: Webpack provides more granular control over polyfills and module transpilation for older browsers
- Complex code splitting: Webpack's code splitting capabilities are more customizable with explicit chunk control
- Micro-frontend architectures: Module Federation, a Webpack-specific feature, enables sharing modules across applications at runtime
- Long-term stability: Webpack's slower release cycle and extensive testing provide stability guarantees that some teams prefer for critical applications
Prerequisites and Preparation
Before starting the migration, ensure you have the following in place:
- Node.js version 16 or higher installed
- A working Vite project with a clear understanding of its current configuration
- Version control committed and pushed to a remote repository
- Familiarity with your project's dependencies and their module formats
- A list of all Vite-specific features currently in use (CSS modules, static imports, env variables, etc.)
Create a migration branch to work on without affecting the main development line. This allows you to experiment freely and revert changes if needed.
Step-by-Step Migration Guide
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Step 1: Remove Vite Dependencies and Configuration
First, uninstall Vite and its related packages from your project:
npm uninstall vite @vitejs/plugin-react @vitejs/plugin-vue vite-plugin-*
# or for yarn
yarn remove vite @vitejs/plugin-react @vitejs/plugin-vue
# or for pnpm
pnpm remove vite @vitejs/plugin-react @vitejs/plugin-vue
Delete the vite.config.js or vite.config.ts file from your project root. Also remove any Vite-specific type references from your tsconfig.json if applicable, such as "types": ["vite/client"].
Step 2: Install Webpack Core Packages
Install Webpack and its core dependencies. The specific packages depend on your project type, but here's a solid foundation:
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev html-webpack-plugin mini-css-extract-plugin css-loader style-loader
For TypeScript projects, add the following:
npm install --save-dev typescript ts-loader
# or if you prefer babel for transpilation
npm install --save-dev @babel/core @babel/preset-env @babel/preset-typescript babel-loader
For React projects, include JSX support:
npm install --save-dev @babel/preset-react
# or if using ts-loader, ensure tsconfig has "jsx": "react-jsx"
For Vue projects:
npm install --save-dev vue-loader vue-template-compiler @vue/compiler-sfc
Step 3: Create the Webpack Configuration File
Create a new file named webpack.config.js in your project root. Start with a basic configuration that mirrors your Vite setup's functionality. Here's a comprehensive starting template for a React + TypeScript project:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
const isDevelopment = argv.mode === 'development';
return {
// Set the mode based on the CLI argument
mode: isProduction ? 'production' : 'development',
// Entry point - equivalent to Vite's index.html served as entry
entry: './src/main.tsx',
// Output configuration
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? 'js/[name].[contenthash:8].js'
: 'js/[name].bundle.js',
chunkFilename: isProduction
? 'js/[name].[contenthash:8].chunk.js'
: 'js/[name].chunk.js',
publicPath: '/',
clean: true, // Cleans output folder before each build
},
// Module resolution - mirrors Vite's resolution behavior
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json', '.css', '.scss'],
alias: {
// Replicate Vite's resolve.alias if you had any
'@': path.resolve(__dirname, 'src'),
},
},
// Loaders define how to process different file types
module: {
rules: [
// TypeScript and JavaScript processing
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: isDevelopment, // Faster rebuilds in dev mode
},
},
},
// JavaScript files (fallback for any .js files)
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
},
},
},
// CSS processing - mimics Vite's CSS handling
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: (resourcePath) => resourcePath.endsWith('.module.css'),
localIdentName: isDevelopment
? '[name]__[local]--[hash:base64:5]'
: '[hash:base64:8]',
},
},
},
'postcss-loader', // Add if you use PostCSS
],
},
// SCSS/SASS processing
{
test: /\.s[ac]ss$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: (resourcePath) =>
resourcePath.endsWith('.module.scss') ||
resourcePath.endsWith('.module.sass'),
localIdentName: isDevelopment
? '[name]__[local]--[hash:base64:5]'
: '[hash:base64:8]',
},
},
},
'sass-loader',
],
},
// Static assets - replicates Vite's asset handling
{
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
// Font files
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]',
},
},
// Handle web workers
{
test: /\.worker\.(ts|js)$/,
use: {
loader: 'worker-loader',
options: { inline: 'no-fallback' },
},
},
],
},
// Plugins handle bundle-level transformations
plugins: [
// Generates the HTML file with script tags injected
new HtmlWebpackPlugin({
template: './index.html',
inject: true,
minify: isProduction
? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
}
: false,
}),
// Extracts CSS into separate files in production
...(isProduction
? [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
]
: []),
],
// Development server configuration
devServer: {
port: 3000,
hot: true,
open: true,
historyApiFallback: true, // Handles client-side routing
static: {
directory: path.join(__dirname, 'public'),
},
client: {
overlay: {
errors: true,
warnings: false,
},
},
},
// Source maps for debugging
devtool: isDevelopment ? 'eval-cheap-module-source-map' : 'source-map',
// Performance hints for large bundles
performance: {
hints: isProduction ? 'warning' : false,
maxAssetSize: 512000,
maxEntrypointSize: 512000,
},
};
};
Step 4: Adapt the HTML Entry Point
Vite uses index.html as the entry point with module script tags. Webpack uses the JavaScript entry point defined in configuration and injects scripts via HtmlWebpackPlugin. Update your index.html to remove Vite-specific script references:
Before (Vite-style):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
After (Webpack-compatible):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="root"></div>
<!-- Scripts are injected automatically by HtmlWebpackPlugin -->
</body>
</html>
Place this index.html at the root of your project. Webpack will use it as a template and inject the bundled scripts automatically.
Step 5: Convert Environment Variable Usage
Vite exposes environment variables through import.meta.env, while Webpack uses process.env via the DefinePlugin. You need to update all references across your codebase and configure Webpack accordingly.
First, update your code references. Replace all instances of:
// Vite-style environment access
const apiUrl = import.meta.env.VITE_API_URL;
const isProduction = import.meta.env.PROD;
With Webpack-compatible patterns:
// Webpack-style environment access
const apiUrl = process.env.VITE_API_URL;
const isProduction = process.env.NODE_ENV === 'production';
Next, add the DefinePlugin to your webpack.config.js to inject these variables at compile time:
const webpack = require('webpack');
// Inside the plugins array, add:
plugins: [
// ... other plugins ...
new webpack.DefinePlugin({
'process.env.VITE_API_URL': JSON.stringify(process.env.VITE_API_URL || ''),
'process.env.VITE_APP_TITLE': JSON.stringify(process.env.VITE_APP_TITLE || 'My App'),
'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'),
// Map all your VITE_* variables similarly
}),
],
Create a .env file at your project root for default values, and use dotenv-webpack to load it automatically:
npm install --save-dev dotenv-webpack
// In webpack.config.js
const Dotenv = require('dotenv-webpack');
plugins: [
// ... other plugins ...
new Dotenv({
path: './.env',
safe: true, // Load only variables defined in .env.example
systemvars: true, // Also load system environment variables
}),
],
Step 6: Handle CSS Modules and Style Imports
Vite has built-in CSS module support with the *.module.css naming convention. Webpack requires explicit configuration through css-loader options, which we already set up in Step 3. However, you may need to adjust your imports if your project uses the Vite-specific ?module query parameter:
// Vite-specific: import styles from './styles.css?module'
// Webpack standard: import styles from './styles.module.css'
If you have many files using the ?module suffix, update them to use the .module.css extension instead. For projects using CSS modules extensively, verify that the localIdentName configuration in your css-loader options produces class names compatible with your existing code and any snapshot tests you may have.
Step 7: Adapt Static Asset Imports
Vite allows importing static assets with special query parameters (like ?url, ?raw, ?inline). Webpack handles these differently. Here's how to adapt:
// Vite: import logoUrl from './logo.svg?url'
// Webpack: import logoUrl from './logo.svg' (with proper loader config)
// The default behavior with asset/resource type gives you the URL
// Vite: import rawContent from './file.txt?raw'
// Webpack equivalent:
// Add to webpack.config.js module.rules:
{
test: /\.txt$/,
type: 'asset/source',
}
// Then simply: import rawContent from './file.txt'
// Vite: import worker from './worker.js?worker'
// Webpack equivalent using worker-loader (already configured in Step 3):
// import worker from './worker.worker.js'
Update your import statements throughout the codebase to remove Vite-specific query parameters. The asset handling rules configured in Step 3 will take care of the rest.
Step 8: Configure Code Splitting and Lazy Loading
Vite automatically splits code based on dependency patterns and dynamic imports. Webpack requires explicit configuration for optimal chunk splitting. Add the following optimization configuration to your webpack.config.js:
optimization: {
// Extract runtime code into a separate chunk
runtimeChunk: 'single',
// Define how modules are split into chunks
splitChunks: {
chunks: 'all',
cacheGroups: {
// Extract vendor/node_modules code
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
// Extract common chunks shared across pages
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true,
},
// Extract large CSS files
styles: {
name: 'styles',
test: /\.(css|scss)$/,
chunks: 'all',
enforce: true,
},
},
},
// Minimizer configuration for production
minimizer: [
'...', // Extends default minimizers
new CssMinimizerPlugin(), // Add if you install css-minimizer-webpack-plugin
],
},
Your existing dynamic imports using import() will continue to work seamlessly. Webpack automatically creates separate chunks for any dynamic import expressions it encounters.
Step 9: Update Package.json Scripts
Replace your Vite-oriented npm scripts with Webpack equivalents. Update the scripts section in package.json:
{
"scripts": {
"start": "webpack serve --mode development --port 3000",
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"analyze": "webpack --mode production --analyze",
"preview": "serve dist -l 4173",
"lint": "eslint src --ext .ts,.tsx",
"type-check": "tsc --noEmit"
}
}
Note that Webpack's development server doesn't have a built-in "preview" mode like Vite's vite preview. You can install serve as a simple static file server for previewing production builds:
npm install --save-dev serve
Step 10: Handle TypeScript Type Declarations
Vite provides client-side type declarations through vite/client. After migration, you need to handle these types differently. Create a src/types/global.d.ts file:
// src/types/global.d.ts
// Replace vite/client type augmentations
// For static asset imports (replaces Vite's asset type declarations)
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
// For workers
declare module '*.worker.ts' {
const WorkerFactory: new () => Worker;
export default WorkerFactory;
}
// Environment variable type declarations
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
VITE_API_URL: string;
VITE_APP_TITLE: string;
// Add all your custom env variables here
}
}
Ensure this file is included in your tsconfig.json:
{
"compilerOptions": {
// ... other options
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/types/**/*.d.ts"]
}
Step 11: Install Additional Loaders and Plugins for Feature Parity
Depending on your Vite project's specific features, you may need additional Webpack equivalents:
# For PostCSS processing (equivalent to Vite's built-in PostCSS support)
npm install --save-dev postcss-loader postcss autoprefixer
# For CSS minimization in production
npm install --save-dev css-minimizer-webpack-plugin
# For environment variable management
npm install --save-dev dotenv-webpack
# For bundle analysis (equivalent to vite-plugin-visualizer)
npm install --save-dev webpack-bundle-analyzer
# For PWA support (equivalent to vite-plugin-pwa)
npm install --save-dev workbox-webpack-plugin
# For copying static assets (equivalent to Vite's public directory)
npm install --save-dev copy-webpack-plugin
# For SVG optimization
npm install --save-dev svgo svgo-loader
Add these to your configuration as needed. For example, adding the copy plugin for static assets:
const CopyPlugin = require('copy-webpack-plugin');
plugins: [
// ... existing plugins ...
new CopyPlugin({
patterns: [
{
from: 'public',
to: '.',
globOptions: {
ignore: ['**/index.html'],
},
},
],
}),
],
Step 12: Test the Migration Thoroughly
After completing the configuration, run through this testing checklist to ensure everything works correctly:
# 1. Start the development server and verify HMR works
npm start
# 2. Build for production and check for errors
npm run build
# 3. Serve the production build locally
npm run preview
# 4. Run your test suite
npm test
# 5. Check bundle sizes
npx webpack-bundle-analyzer dist/stats.json
# 6. Verify TypeScript compilation
npm run type-check
Pay special attention to:
- Dynamic imports and lazy-loaded routes working correctly
- CSS styles applying properly with correct class names
- Static assets loading with correct paths
- Environment variables resolving to expected values
- Source maps pointing to correct locations in development tools
- Production bundle sizes comparable to or better than Vite's output
Common Migration Pitfalls and Solutions
Module Resolution Differences
Vite uses ES module resolution natively, while Webpack uses enhanced resolver with configurable extensions. If you encounter "module not found" errors after migration, check your resolve.extensions and resolve.alias configuration. Ensure file extensions in your imports match the configured extensions or add the missing extension to the array:
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json', '.mjs', '.cjs'],
// ...
}
Handling require() in Mixed Codebases
Vite discourages CommonJS require() calls. Webpack handles them gracefully, but mixing require() and import can lead to interoperability issues. Convert all require() calls to ES module imports for consistency:
// Before: const lodash = require('lodash');
// After: import lodash from 'lodash';
Dev Server Port and Hot Reload Behavior
Webpack's dev server doesn't automatically increment ports like Vite does. Configure an explicit port in devServer.port and ensure no other services occupy it. If HMR doesn't trigger, verify that hot: true is set and that you're not using a WebSocket interfering proxy.
Best Practices for a Smooth Migration
- Migrate incrementally: If your project is large, consider migrating one feature branch at a time rather than the entire codebase at once
- Keep both configs temporarily: Maintain a
vite.config.js.bakreference file during the transition period for quick comparison - Use Webpack's stats output: Run
webpack --json > stats.jsonto get detailed build information for debugging - Leverage source maps: Use
devtool: 'eval-cheap-module-source-map'in development for fast rebuilds with good debugging quality - Optimize loader performance: Set
transpileOnly: trueints-loaderduring development and usefork-ts-checker-webpack-pluginfor type checking in a separate process - Document the migration: Create a MIGRATION.md file detailing all changes, configuration decisions, and any workarounds applied
- Update CI/CD pipelines: Ensure your continuous integration scripts use the new build commands and that artifact paths match Webpack's output structure
- Version pinning: Pin Webpack and plugin versions in
package.jsonto avoid unexpected breaking changes during minor updates
Conclusion
Migrating from Vite to Webpack is a substantial undertaking that requires careful planning and methodical execution. While Vite's developer experience is undeniably smoother with its instant startup and lightning-fast HMR, Webpack offers unparalleled control, a battle-tested plugin ecosystem, and features like Module Federation that are essential for certain enterprise architectures. By following this step-by-step guide, you can systematically replace Vite's configuration with equivalent Webpack setups, handle environment variables properly, adapt asset imports, and configure code splitting to match or exceed your previous build performance. The key to success lies in thorough testing at each step, maintaining backward compatibility during the transition, and leveraging Webpack's extensive configuration options to fine-tune the build pipeline to your project's specific needs. With the configuration template and practices outlined above, you'll have a solid foundation for a production-ready Webpack setup that preserves all the functionality your application requires.