← Back to Blog
React2026-05-05·41 min read

Migrating a legacy React app from webpack to Vite

By Amanda Gama

Migrating a Legacy React App from Webpack to Vite

Current Situation Analysis

The legacy codebase suffered from architectural debt accumulated over five years: React 16 with pervasive class components, React Router v3 using routes-as-children, and a heavily patched webpack 4 configuration maintained by a dozen developers. The development loop was critically degraded: cold starts took 45 seconds, HMR cycles ranged from 8 to 20 seconds, and production builds consumed 6 minutes. CI/CD pipelines compounded the bottleneck, resulting in 14-minute end-to-end deploy times.

Webpack's fundamental model requires upfront parsing, compilation, and bundling of the entire dependency graph before serving. This scales linearly with application size, and HMR graph invalidation turns minor code changes into multi-second rebuilds. Traditional optimization strategies (cache tuning, loader splitting, parallel compilation) hit diminishing returns because the bottleneck is architectural, not configurational.

The critical realization is that migrating to Vite is not a standalone tool swap. Vite's performance derives from native ES module serving and on-demand transformation via esbuild, which inherently rejects 2018-era tooling assumptions. Attempting a drop-in replacement fails because Vite enforces modern module resolution, JSX transforms, and dependency optimization. The migration inevitably cascades into upgrading React, rewriting the router, and auditing the dependency tree.

WOW Moment: Key Findings

The performance delta stems from shifting compilation from upfront bundling to on-demand transformation. Production still uses Rollup for optimal tree-shaking, but the dev loop eliminates graph compilation entirely. Secondary CI improvements arise from aggressive tree-shaking (18% bundle reduction), elimination of 800MB persistent cache thrashing, and parallelizable pipeline stages.

Approach Dev Cold Start HMR Latency (Small Change) Prod Build Time
Webpack 4 42s 4-8s 5m 40s
Vite + esbuild/Rollup 1.1s 50-200ms 1m 50s

Key Findings:

  • Transformation vs. Validation: Vite strips types via esbuild for speed. Static type checking remains unchanged (tsc --noEmit still takes ~45s). Vite optimizes transformation, not analysis.
  • CI Multiplier Effect: Deploy time dropped from 14m to 5m. Only 4m came from build acceleration; the rest came from parallel job execution, cache elimination, and faster Docker layer pushes due to smaller bundles.
  • Sweet Spot: Vite delivers maximum ROI when paired with a modernized stack. The performance ceiling is unlocked by aligning tooling with native ESM expectations rather than patching legacy bundler behavior.

Core Solution

The migration follows a phased architectural shift: replace the build orchestrator, restructure the entry point, upgrade core framework dependencies, and modernize routing patterns.

1. Build Tool Configuration

Install the core Vite package and the official React plugin:

npm install --save-dev vite @vitejs/plugin-react

A minimal vite.config.ts establishes aliases, server defaults, and build output settings:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
})

2. Entry Point Restructuring

Webpack treats index.html as a generated template. Vite treats it as the actual entry point. Move index.html to the project root and replace template injection with a direct module script:

<script type="module" src="/src/main.tsx"></script>

3. Framework & Router Modernization

@vitejs/plugin-react requires React 17+ JSX transform. Upgrade to React 18 to leverage Strict Mode and catch legacy effect bugs.

React Router v3's imperative API (browserHistory, onEnter, withRouter) is incompatible with v6's declarative <Routes> model. Migrate routing configuration:

// v3
<Router history={browserHistory}>
  <Route path="/" component={App}>
    <Route path="users/:id" component={UserPage} onEnter={requireAuth} />
  </Route>
</Router>

// v6
<BrowserRouter>
  <Routes>
    <Route path="/" element={<App />}>
      <Route path="users/:id" element={<RequireAuth><UserPage /></RequireAuth>} />
    </Route>
  </Routes>
</BrowserRouter>

Imperative navigation (browserHistory.push()) and HOC wrappers must be manually refactored to useNavigate and component composition.

4. Dependency & Plugin Audit

Remove legacy Babel plugins for syntax already supported natively by esbuild (optional chaining, nullish coalescing, class properties). Custom transforms must be rewritten as Vite plugins or extracted to separate build steps. Audit pure CommonJS dependencies lacking exports fields; they will conflict with Vite's dep optimizer. Apply patch-package or replace unmaintained packages.

Pitfall Guide

  1. Assuming Vite Handles Type Checking: Vite strips TypeScript types via esbuild for transformation speed. It does not perform static analysis. You must run tsc --noEmit as a separate step in CI to maintain type safety.
  2. Treating index.html as a Webpack Template: Vite uses index.html as the actual module entry point. Failing to add type="module" to script tags or pointing them to the correct TS entry will break the dev server's ESM resolution.
  3. Ignoring the React 17+ JSX Transform Requirement: @vitejs/plugin-react relies on the automatic JSX runtime introduced in React 17. Legacy React 16 setups will fail compilation unless upgraded or pinned to an incompatible legacy plugin version.
  4. Overlooking Imperative Router Patterns in v3→v6 Migration: Vite migration often forces a router upgrade. v3's browserHistory.push() and withRouter HOCs break in v6's declarative model. Every imperative navigation call requires manual refactoring to useNavigate and route component composition.
  5. Fighting Vite's Dep Optimizer with Pure CommonJS: Legacy dependencies without an exports field or proper ESM entry points will cause resolution failures. Vite's pre-bundling expects modern module formats. Use optimizeDeps.include in config, apply patch-package, or replace abandoned packages.
  6. Misconfiguring Environment Variable Exposure: Webpack leaks any process.env reference. Vite replaces process.env.FOO with import.meta.env.VITE_FOO and strictly exposes only VITE_-prefixed variables to the client. Unprefixed variables are stripped for security, breaking legacy env configurations.

Deliverables

📘 Migration Blueprint A phased execution roadmap for legacy webpack→Vite transitions:

  • Phase 1: Config swap & entry point restructuring
  • Phase 2: React 18 upgrade & JSX transform alignment
  • Phase 3: Router v3→v6 declarative rewrite
  • Phase 4: Dependency optimizer audit & Babel plugin removal
  • Phase 5: CI pipeline parallelization & cache strategy update

✅ Implementation Checklist

  • Install vite + @vitejs/plugin-react and remove webpack CLI/loaders
  • Create vite.config.ts with React plugin, path aliases, and build sourcemaps
  • Move index.html to root and convert script tag to type="module"
  • Upgrade React to 17+ (preferably 18) and enable Strict Mode
  • Refactor React Router v3 imperative patterns to v6 <Routes> + useNavigate
  • Remove redundant Babel plugins; migrate custom transforms to Vite plugins
  • Audit CommonJS dependencies; configure optimizeDeps or apply patches
  • Replace process.env with import.meta.env.VITE_* and enforce prefix policy
  • Update CI: split type-check/lint/build into parallel jobs, remove webpack cache restore step
  • Validate HMR latency, cold start times, and production bundle size reduction