<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/webpack-to-vite-migration-large-codebases-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/webpack-to-vite-migration-large-codebases-2026/raw.md -->
<!-- Source path: content/guides/webpack-to-vite-migration-large-codebases-2026.mdx -->

---
og_image: "/images/guides/webpack-to-vite-migration-large-codebases-2026.webp"
title: "Migrating Webpack to Vite 2026: Large App Guide"
description: "Step-by-step guide to migrating large Webpack codebases to Vite in 2026. Covers Module Federation, custom loaders, monorepos, and the most common blockers with."
date: "2026-03-16"
author: "PkgPulse Team"
tags: ["vite", "webpack", "migration", "bundler", "monorepo", "javascript", "2026"]
featured_comparison: "vite-vs-webpack"
---

Migrating a small app from Webpack to Vite takes an afternoon. Migrating a large codebase — 200K+ LOC, dozens of custom Webpack plugins, Module Federation micro-frontends, a legacy CJS codebase — takes weeks or months of careful planning. The performance gains are real (50-100x faster dev server startup on large projects), but the path isn't "swap bundler, done."

This guide is for the teams that can't just follow the 5-step migration tutorial. It covers incremental migration strategies, the blockers you'll actually hit, and how to handle the edge cases that sink large migrations.

## TL;DR

The key insight for large codebase migrations: **don't do it all at once**. Use Vite's `legacy` and `commonjs` plugins to handle the 80% that "just works," create a compatibility shim layer for custom Webpack behaviors, and migrate Module Federation last (it's the hardest part). Expect 2-6 weeks for a 100K+ LOC production app with a dedicated team. The dev server speedup (from 60+ seconds to under 2 seconds) makes the investment worth it for most teams.

## Key Takeaways

- **Dev server startup**: Webpack 60-120s → Vite <2s on 100K+ LOC projects — the primary motivator
- **Vite 8 + Rolldown** (March 12, 2026): Rust-based unified bundler replaces both Rollup (prod) and esbuild (dev) — 10-30x faster production builds, true dev/prod parity
- **Biggest blockers**: `require()` in app code, Module Federation, custom Webpack loaders with no Vite equivalent, `process.env` patterns, circular dependencies
- **Incremental strategy**: run Webpack (production) + Vite (development) simultaneously during migration
- **Module Federation**: use `@module-federation/vite` plugin — functionally equivalent but requires config migration
- **CommonJS**: `vite-plugin-commonjs` handles most cases; true CJS app code needs rewriting

## At a Glance: What Changes

| Webpack Pattern | Vite Equivalent | Notes |
|----------------|-----------------|-------|
| `process.env.REACT_APP_X` | `import.meta.env.VITE_X` | Prefix change required |
| `webpack.DefinePlugin` | `define` in vite.config.ts | Same purpose, different API |
| `require('./file')` (dynamic) | `await import('./file')` | CJS → ESM |
| `require.context()` | `import.meta.glob()` | Glob imports |
| `webpack.ProvidePlugin` | `vite-plugin-node-polyfills` | Node.js polyfills |
| `ModuleFederationPlugin` | `@module-federation/vite` | Plugin available |
| `url-loader` / `file-loader` | Built-in (Vite handles assets natively) | No plugin needed |
| `babel-loader` | `@vitejs/plugin-react` or `@vitejs/plugin-vue` | Framework plugin |
| Custom loaders | Custom Vite plugins (`transform` hook) | Different API |
| `splitChunks` | `build.rollupOptions.output.manualChunks` | Different syntax |

---

## Phase 1: Audit Before You Migrate

Before touching config files, run a full audit to understand what you're dealing with.

### Audit Checklist

```bash
# 1. Find all require() calls in application code (not just node_modules)
grep -r "require(" src/ --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" | wc -l

# 2. Find require.context() usage (Webpack-specific)
grep -r "require\.context" src/ --include="*.js" --include="*.ts"

# 3. Find process.env usage (needs prefix migration)
grep -r "process\.env\." src/ --include="*.js" --include="*.ts" | grep -v "NODE_ENV" | wc -l

# 4. List all Webpack plugins (each needs a Vite equivalent or workaround)
cat webpack.config.js | grep -A2 "plugins:"

# 5. Find circular dependencies (will fail loudly in Vite)
npx madge --circular src/ --extensions ts,tsx,js,jsx

# 6. Find dynamic requires (hardest to migrate)
grep -r "require(variable\|require(path\.\|require(\`" src/ --include="*.js" --include="*.ts"
```

### Common Audit Findings in Large Codebases

| Issue | Frequency | Migration Effort |
|-------|-----------|-----------------|
| `process.env` references | Very high | Low (search/replace) |
| Static `require()` calls | High | Medium (script + manual review) |
| Dynamic `require()` calls | Medium | High (case-by-case) |
| `require.context()` | Medium | Low (glob equivalent) |
| Custom loaders | Varies | High (rewrite as Vite plugins) |
| Module Federation | Medium | High (dedicated phase) |
| Circular dependencies | Often discovered | High (fix the code) |
| Node.js built-in polyfills | Medium | Low (polyfill plugin) |

---

## Phase 2: The Incremental Migration Strategy

**Don't replace Webpack overnight.** For large teams, the safest approach is running both bundlers simultaneously during migration.

### Strategy: Vite for Dev, Webpack for Prod

```
Week 1-2: Get Vite running in development mode
Week 3-4: Resolve blockers, migrate most files
Week 5-6: Switch production builds to Vite
Week 7+: Remove Webpack
```

```json
// package.json — dual bundler scripts during migration
{
  "scripts": {
    "dev": "vite",                    // ← New: Vite dev server
    "dev:webpack": "webpack serve",   // ← Keep: fallback during migration
    "build": "webpack --config webpack.config.js",  // ← Still using Webpack for prod
    "build:vite": "vite build",       // ← Test Vite prod builds incrementally
    "build:types": "tsc --noEmit"
  }
}
```

This means developers get the fast Vite dev server immediately, while production continues shipping from Webpack until you're confident the Vite build is correct.

---

## Phase 3: Install and Configure Vite

```bash
npm install -D vite @vitejs/plugin-react
# Or for Vue: @vitejs/plugin-vue
# For legacy browser support: @vitejs/plugin-legacy
```

```typescript
// vite.config.ts — starting template for large React apps
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [react()],

    // Path aliases (mirrors webpack resolve.alias)
    resolve: {
      alias: {
        '@': resolve(__dirname, './src'),
        '@components': resolve(__dirname, './src/components'),
        '@utils': resolve(__dirname, './src/utils'),
        // Add your existing webpack aliases here
      },
    },

    // Environment variables (replaces webpack.DefinePlugin)
    define: {
      // Migrate process.env.NODE_ENV usage
      'process.env.NODE_ENV': JSON.stringify(mode),
      // Legacy support while migrating (removes gradually)
      __WEBPACK_DEV_SERVER__: JSON.stringify(false),
    },

    // Dev server config
    server: {
      port: 3000,
      proxy: {
        // Proxy API calls during development
        '/api': {
          target: 'http://localhost:8080',
          changeOrigin: true,
        },
      },
    },

    // Build config
    build: {
      rollupOptions: {
        output: {
          // Manual chunks to match webpack's splitChunks behavior
          manualChunks: {
            vendor: ['react', 'react-dom'],
            // Add other large dependencies
          },
        },
      },
    },
  }
})
```

---

## Phase 4: The Hard Blockers

### Blocker 1: CommonJS require() in App Code

Vite's dev server serves ES modules natively. `require()` in your app code (not node_modules) will fail.

**Approach A: Plugin-based compatibility (fast, temporary)**

```bash
npm install -D vite-plugin-commonjs
```

```typescript
// vite.config.ts
import commonjs from 'vite-plugin-commonjs'

export default defineConfig({
  plugins: [
    commonjs(), // Converts require() calls to import() at dev time
    react(),
  ],
})
```

This buys time but doesn't fix the underlying issue. Create a script to systematically migrate:

```bash
# Use codemod to migrate static requires
npx codemod --transform @codemod/transform-cjs-to-esm src/
# Review and manually fix edge cases
```

**Approach B: Codemod the requires (permanent)**

```javascript
// Before (CommonJS)
const { utils } = require('./utils')
const config = require('../config.json')
const LazyComponent = require('./LazyComponent').default

// After (ESM)
import { utils } from './utils'
import config from '../config.json' with { type: 'json' }
import LazyComponent from './LazyComponent'
```

### Blocker 2: `require.context()` for Glob Imports

Webpack's `require.context()` is commonly used to dynamically import multiple modules:

```javascript
// Before (Webpack)
const context = require.context('./components', true, /\.vue$/)
const modules = context.keys().map(key => context(key))

// After (Vite)
const modules = import.meta.glob('./components/**/*.vue', { eager: true })
// OR lazy (returns functions returning Promises)
const lazyModules = import.meta.glob('./components/**/*.vue')
```

```javascript
// Common pattern: auto-register Vue/React components
// Before (Webpack)
const requireComponent = require.context('./components', false, /Base[A-Z]\w+\.(vue|js)$/)
requireComponent.keys().forEach(fileName => {
  const component = requireComponent(fileName)
  const componentName = path.basename(fileName, path.extname(fileName))
  app.component(componentName, component.default || component)
})

// After (Vite)
const components = import.meta.glob('./components/Base*.vue', { eager: true })
Object.entries(components).forEach(([path, module]) => {
  const componentName = path.split('/').pop().replace('.vue', '')
  app.component(componentName, module.default)
})
```

### Blocker 3: `process.env` Variable Migration

The rename from `REACT_APP_` to `VITE_` is the most common migration task:

```bash
# Find all env var references in your codebase
grep -r "process\.env\.REACT_APP_" src/ --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" -l

# Batch rename with sed (macOS)
find src/ -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" | \
  xargs sed -i '' 's/process\.env\.REACT_APP_/import.meta.env.VITE_/g'

# Don't forget .env files
find . -name ".env*" -not -path "*/node_modules/*" | \
  xargs sed -i '' 's/^REACT_APP_/VITE_/g'
```

### Blocker 4: Module Federation

Module Federation is the hardest part of large codebase migrations. The `@module-federation/vite` plugin provides a compatible API:

```bash
npm install -D @module-federation/vite
```

```typescript
// Shell app vite.config.ts
import { federation } from '@module-federation/vite'

export default defineConfig({
  plugins: [
    federation({
      name: 'shell',
      remotes: {
        // Same as webpack's ModuleFederationPlugin remotes
        'mfe-products': 'http://localhost:3001/remoteEntry.js',
        'mfe-cart': 'http://localhost:3002/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    target: 'esnext', // Required for Module Federation
  },
})
```

```typescript
// Remote app vite.config.ts
import { federation } from '@module-federation/vite'

export default defineConfig({
  plugins: [
    federation({
      name: 'mfe-products',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
})
```

**Known limitation:** `@module-federation/vite` works well for production builds but has some rough edges in Vite's dev server. Test thoroughly before cutting over.

### Blocker 5: SVG and Asset Imports

If you're using `@svgr/webpack` to import SVGs as React components, you'll hit this immediately:

```bash
npm install -D vite-plugin-svgr
```

```typescript
// vite.config.ts
import svgr from 'vite-plugin-svgr'

export default defineConfig({
  plugins: [svgr(), react()],
})
```

```javascript
// After: use ?react query suffix
import Logo from './logo.svg?react'

// Or configure default to always treat SVGs as components:
// svgr({ include: '**/*.svg' })  ← then import without suffix
```

For `file-loader` / `url-loader` patterns (inline base64, asset URLs): Vite handles these natively. Import any asset and you get its URL:

```javascript
import logoUrl from './logo.png'  // '/assets/logo-a1b2c3.png'
```

### Blocker 6: Custom Webpack Loaders

Each custom Webpack loader needs to be rewritten as a Vite plugin:

```javascript
// Webpack loader: my-svg-loader.js
module.exports = function(source) {
  return source.replace(/fill="#[0-9a-fA-F]{6}"/g, 'fill="currentColor"')
}

// Vite plugin equivalent:
// vite.config.ts
function svgCurrentColorPlugin() {
  return {
    name: 'svg-current-color',
    transform(code, id) {
      if (!id.endsWith('.svg')) return
      return code.replace(/fill="#[0-9a-fA-F]{6}"/g, 'fill="currentColor"')
    },
  }
}

export default defineConfig({
  plugins: [svgCurrentColorPlugin(), react()],
})
```

---

## TypeScript Path Aliases and tsconfig Migration

TypeScript path aliases configured in `tsconfig.json` under `compilerOptions.paths` require explicit reproduction in `vite.config.ts` as `resolve.alias` entries — Vite does not automatically read `tsconfig.json` path aliases during the dev server or build. This is one of the most common sources of "works in tsc but fails in Vite" errors during migration. The `vite-tsconfig-paths` plugin automates this: it reads your `tsconfig.json` (and the referenced tsconfig files in a monorepo) and applies all path aliases to Vite's resolver automatically. For large codebases with dozens of path aliases across multiple tsconfig files, this plugin is effectively mandatory during migration to avoid manual duplication and the drift that comes when alias additions are only added to one config.

Project references in TypeScript monorepos (where `tsconfig.json` files use `references` to point to sibling packages) interact with Vite's module resolution in ways that require careful configuration. Vite's `optimizeDeps.include` array should list internal workspace packages when their source TypeScript is resolved directly (rather than through their built `dist/` output) — this tells Vite's esbuild pre-bundling step to process those packages as part of the dependency optimization, avoiding slow cold-start issues on first load. Teams using Nx or Turborepo that have configured TypeScript project references as the primary module resolution mechanism should test Vite's dev server thoroughly with the `vite-tsconfig-paths` plugin before removing any `resolve.alias` configuration.

## Testing Pipelines During Migration

A frequently overlooked aspect of large Webpack-to-Vite migrations is the impact on the testing pipeline. Jest (still common in large codebases) uses Babel or ts-jest for transformation and has no dependency on Webpack or Vite — Jest tests should continue working unchanged during the migration. However, if your test suite uses Webpack-specific features like `require.context` for automatic test discovery, or if it imports from `process.env.WEBPACK_DEV_SERVER` for environment detection, those tests will need updates alongside the main codebase.

Vitest is the natural companion to Vite for testing — it uses the same `vite.config.ts` configuration and shares the module resolution setup, meaning path aliases, environment variable handling, and plugin transforms work identically in tests and in the app. Migrating from Jest to Vitest is optional during the Webpack-to-Vite migration and is best treated as a separate effort, but teams that migrate both simultaneously report that test reliability improves because test and production code share the same transformation pipeline. For teams keeping Jest, the `vitest` migration can be deferred while still getting the full Vite dev server speedup — Jest's configuration is independent of the build tool.

## Vite 8 + Rolldown: The Large Codebase Game-Changer

Vite 8 (released March 12, 2026) ships Rolldown as a unified bundler, replacing both Rollup (production) and esbuild (development transforms). Rolldown is built in Rust by the VoidZero team — the impact on large codebases is significant:

Real-world production results from Vite 8's March 2026 GA release:

```
Real-world build improvements (Vite 8 + Rolldown):
  Linear's production build:  46s → 6s  (7.7x faster)
  Generic large project (500+ modules): 10-30x faster builds
  HMR patch time: ~60s → <50ms
  Memory usage: up to 80% reduction vs Webpack

Dev/prod parity: same Rolldown engine in both modes
  → Eliminates entire class of "works in dev, breaks in prod" issues
```

**Vite 8 requires Node.js 20.19+ or 22.12+.** Audit your `.nvmrc`, CI base images, and `engines` field in `package.json` before upgrading.

### Vite 8 Breaking Changes for Large Apps

If you're migrating directly to Vite 8, or upgrading from Vite 7:

```typescript
// Vite 7: rollupOptions
build: {
  rollupOptions: {
    output: { manualChunks: { vendor: ['react', 'react-dom'] } }
  }
}

// Vite 8: rolldownOptions (compat shim exists but explicit migration preferred)
build: {
  rolldownOptions: {
    output: {
      advancedChunks: {
        groups: [
          { name: 'react-vendor', test: /react|react-dom/ },
          { name: 'vendor', test: /node_modules/ },
        ]
      }
    }
  }
}
```

Other Vite 8 changes that hit large apps:
- **Stricter CJS interop** — default imports from CJS packages may break; use `legacy.inconsistentCjsInterop: true` as a temporary shim
- **Custom plugin hooks** — `moduleType: 'js'` required in some `transform` hooks
- **HMR API change** — `import.meta.hot.accept` no longer accepts a URL string

**Recommended two-step upgrade**: Install `rolldown-vite` as a drop-in first to validate Rolldown-specific issues, then upgrade to Vite 8 for the remaining Vite-level breaking changes.

If you're evaluating whether to migrate in 2026, Vite 8 + Rolldown closes the last remaining criticism of Vite for large apps. The case for migration is now stronger than ever.

---

## Monorepo Considerations

For Nx, Turborepo, or custom monorepos:

```typescript
// packages/web-app/vite.config.ts — shared config pattern
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Reference sibling packages directly in monorepo
      '@company/ui': resolve(__dirname, '../../packages/ui/src'),
      '@company/utils': resolve(__dirname, '../../packages/utils/src'),
    },
  },
  // Optimize pre-bundling for monorepo packages
  optimizeDeps: {
    include: ['@company/ui', '@company/utils'],
  },
})
```

```json
// turbo.json — include vite cache
{
  "pipeline": {
    "build": {
      "outputs": [".vite/**", "dist/**"],
      "inputs": ["src/**", "vite.config.ts", "index.html"]
    }
  }
}
```

---

## Migration Metrics: What to Expect

Real-world and community migration reports for large codebases:

| Project / Metric | Before (Webpack) | After (Vite 8) | Source |
|-----------------|-----------------|----------------|--------|
| Linear prod build | 46s | 6s (7.7x) | Vite 8 release notes |
| Swimm web app cold start | 2+ min | Near-instant | Swimm blog |
| ~500 components / 150K LOC HMR | ~60s | <50ms | Community benchmark |
| Generic dev cold start | 60-120s | 0.5-2s | Multiple reports |
| Generic prod build (Vite 8) | 45-90s | 3-8s | Multiple reports |
| CI build time | 2-4 min | 20-40s | Multiple reports |
| Memory usage | Baseline | -50-80% | Rolldown benchmarks |
| Bundle size | Baseline | -5-15% (tree-shaking) | Multiple reports |

### Bundle Analysis Migration

Replace `webpack-bundle-analyzer` with `rollup-plugin-visualizer`:

```bash
npm install -D rollup-plugin-visualizer
```

```typescript
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({ filename: 'dist/stats.html', open: true }),
  ],
})
```

Run `vite build` and `dist/stats.html` opens with an interactive treemap — same workflow as `webpack-bundle-analyzer`.

---

*Compare Vite vs Webpack download trends on [PkgPulse](https://www.pkgpulse.com/compare/vite-vs-webpack).*

*Related: [Vite vs Rspack vs Webpack 2026](/guides/vite-vs-rspack-vs-webpack-bundler-2026) · [How to Migrate Webpack to Vite](/guides/how-to-migrate-webpack-to-vite-2026) · [Vite vs Webpack: Is Migration Worth It?](/guides/vite-vs-webpack-2026)*
