← Back to DevBytes

Migrating from Vite to Webpack: Step-by-Step Guide

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:

Prerequisites and Preparation

Before starting the migration, ensure you have the following in place:

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:

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles