Skip to main content
Modernization
11 min read
November 5, 2025

The Complete Guide to Migrating from Create React App to Vite

Step-by-step instructions to modernize your React build pipeline

Segev Sinay

Segev Sinay

Frontend Architect

Share:

Create React App is dead. React itself has removed CRA from its official documentation, the repository is barely maintained, and the developer experience has fallen years behind modern alternatives. If you're still running CRA in production, you're dealing with painfully slow builds, bloated configurations, and a toolchain that's holding your team back.

The good news: migrating to Vite is one of the highest-ROI changes you can make to your frontend codebase. I've done this migration for multiple clients, and the results are consistently dramatic — 10-20x faster dev server startup, 5-10x faster production builds, and significantly happier developers.

This guide walks through the entire migration process, step by step, based on real-world experience.

Why Migrate?

Before we dive into the how, let's be clear about the why.

Build Performance

Here are real numbers from a recent client migration (medium-sized SaaS app, ~800 components):

| Metric | CRA | Vite | Improvement | |---|---|---|---| | Dev server cold start | 45s | 1.2s | 37x faster | | Hot Module Replacement | 4-8s | <50ms | ~100x faster | | Production build | 3m 20s | 22s | 9x faster | | Bundle size | 2.1MB | 1.4MB | 33% smaller |

These aren't cherry-picked numbers. They're typical for a medium-complexity React application.

Developer Experience

  • Instant server start. Vite serves source files over native ESM — no bundling required during development.
  • True HMR. Changes reflect in the browser in under 50ms, regardless of app size.
  • Better error messages. Vite's error overlay is clearer and more actionable than CRA's.
  • Modern defaults. ESM, tree-shaking, code splitting — all optimized out of the box.

Ecosystem Support

CRA's last meaningful update was over two years ago. Vite, by contrast, has an active ecosystem with plugins for everything you need, first-class TypeScript support, and a community that's shipping improvements weekly.

Pre-Migration Checklist

Before you start, audit your current setup:

  • [ ] Node.js version: Vite requires Node 18+. Check with node --version.
  • [ ] Custom webpack config? If you've ejected CRA or use craco/react-app-rewired, list every customization. Each one needs a Vite equivalent.
  • [ ] Environment variables: List all REACT_APP_* variables. They'll need renaming.
  • [ ] Testing setup: If you're using Jest (CRA default), plan for the testing migration.
  • [ ] Proxy configuration: If you have a proxy setup in package.json, note the target URLs.
  • [ ] CI/CD pipeline: Note all build-related commands in your pipeline.

Step-by-Step Migration

Step 1: Install Vite and Dependencies

# Remove CRA dependencies
npm uninstall react-scripts

# Install Vite and plugins
npm install -D vite @vitejs/plugin-react

# If using TypeScript (you should be)
npm install -D @types/node

Step 2: Create Vite Configuration

Create vite.config.ts in your project root:

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, // Match your current CRA port
    proxy: {
      // If you had a proxy in package.json
      '/api': {
        target: 'http://localhost:3002',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'build', // Match CRA's output directory, or change to 'dist'
    sourcemap: true,
  },
});

Step 3: Move and Update index.html

CRA keeps index.html in the public/ folder. Vite expects it at the project root.

mv public/index.html ./index.html

Then update index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Your App</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- This is the key addition - Vite needs this script tag -->
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>

Key changes:

  • Remove %PUBLIC_URL% references — Vite handles public assets automatically
  • Add the <script type="module"> tag pointing to your entry file
  • Remove any %REACT_APP_*% template variables from HTML

Step 4: Update Environment Variables

This is the most error-prone step. CRA uses REACT_APP_ prefix; Vite uses VITE_.

# .env (before)
REACT_APP_API_URL=https://api.example.com
REACT_APP_GA_ID=G-XXXXXXXXXX
REACT_APP_SENTRY_DSN=https://xxx@sentry.io/xxx

# .env (after)
VITE_API_URL=https://api.example.com
VITE_GA_ID=G-XXXXXXXXXX
VITE_SENTRY_DSN=https://xxx@sentry.io/xxx

Then update every reference in your code:

// Before
const apiUrl = process.env.REACT_APP_API_URL;

// After
const apiUrl = import.meta.env.VITE_API_URL;

Pro tip: Use a find-and-replace across your entire codebase. But do it in two passes:

  1. Replace process.env.REACT_APP_ with import.meta.env.VITE_
  2. Manually review each change — some might be in conditional logic that needs attention

For TypeScript, add type definitions. Create or update src/vite-env.d.ts:

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_GA_ID: string;
  readonly VITE_SENTRY_DSN: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Step 5: Update package.json Scripts

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext ts,tsx"
  }
}

Remove any CRA-specific scripts (react-scripts start, react-scripts build, react-scripts test).

Step 6: Handle Static Assets

CRA has special handling for assets in the public/ folder and imports from src/. Vite handles these differently:

// Importing images/assets - works the same way
import logo from './logo.svg';

// But for assets in public/, remove %PUBLIC_URL%
// Before (in JSX): <img src={process.env.PUBLIC_URL + '/logo.png'} />
// After: <img src="/logo.png" />

Step 7: Update SVG Handling

CRA includes built-in SVG-as-component support. With Vite, you need a plugin:

npm install -D vite-plugin-svgr
// vite.config.ts
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [
    react(),
    svgr(), // Add this
  ],
});
// Now SVG imports work like CRA
import { ReactComponent as Logo } from './logo.svg'; // Named import
import logoUrl from './logo.svg'; // URL import (default)

Step 8: Update Path Aliases

If you were using CRA's built-in src/ import support or craco aliases:

// tsconfig.json - add/update paths
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

The Vite config alias we set up in Step 2 handles the runtime resolution. The tsconfig paths handle TypeScript's understanding of the aliases.

Migrating Your Test Setup

CRA ships with Jest pre-configured. When you remove react-scripts, your test setup breaks. You have two options:

Option A: Keep Jest (Less Migration Work)

npm install -D jest @testing-library/react @testing-library/jest-dom
npm install -D ts-jest @types/jest identity-obj-proxy

Create jest.config.ts:

export default {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/src/__mocks__/fileMock.ts',
  },
  setupFilesAfterSetup: ['<rootDir>/src/setupTests.ts'],
};

Option B: Migrate to Vitest (Recommended)

Vitest is built on top of Vite, so it shares your Vite config and offers near-instant test startup:

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
// vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts',
    css: true,
  },
});

The best part: Vitest is API-compatible with Jest. Most of your existing tests will work without changes. The main differences:

// Jest: globals available automatically
// Vitest: import explicitly (or set globals: true in config)
import { describe, it, expect, vi } from 'vitest';

// jest.fn() → vi.fn()
// jest.mock() → vi.mock()
// jest.spyOn() → vi.spyOn()

CI/CD Changes

Your CI pipeline will need updates. Here's a typical before/after:

# Before (GitHub Actions with CRA)
- name: Build
  run: npm run build
  env:
    CI: true
    REACT_APP_API_URL: ${{ secrets.API_URL }}

# After (GitHub Actions with Vite)
- name: Build
  run: npm run build
  env:
    VITE_API_URL: ${{ secrets.API_URL }}

Key changes:

  • Remove CI=true (CRA used this to treat warnings as errors — configure ESLint separately if you want this behavior)
  • Rename all REACT_APP_* env vars to VITE_*
  • Update build output directory if you changed it (build/ vs dist/)
  • Update any deploy commands that reference the output directory

Common Pitfalls

1. Global CSS Import Order

Vite processes CSS imports differently than CRA. If you have CSS ordering issues after migration, make sure your global CSS is imported first in your entry file:

// src/main.tsx (entry point)
import './index.css'; // Global styles first
import './App.css';
import App from './App';

2. process.env References

Vite doesn't polyfill process.env like CRA does. If you have library code that references process.env.NODE_ENV, add a define in your Vite config:

export default defineConfig({
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  },
});

3. CommonJS Dependencies

Some older npm packages use CommonJS (require()) instead of ESM (import). Vite handles most of these automatically through its dependency pre-bundling, but occasionally you'll hit issues. The fix is usually adding the problematic package to optimizeDeps:

export default defineConfig({
  optimizeDeps: {
    include: ['problematic-package'],
  },
});

4. Dynamic Imports with Variables

CRA (webpack) supports dynamic imports with expressions. Vite is stricter:

// This works in CRA but may fail in Vite
const module = await import(`./pages/${pageName}`);

// Vite needs glob imports for this pattern
const pages = import.meta.glob('./pages/*.tsx');
const module = await pages[`./pages/${pageName}.tsx`]();

5. Missing TypeScript Config for Vite

Make sure your tsconfig includes Vite's client types. Remove react-scripts from types if it's there:

{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

Performance Comparison: Real Numbers

After completing the migration for a client's 800-component SaaS application, we benchmarked everything:

Development:

  • Cold start: 45s -> 1.2s (the single biggest quality-of-life improvement)
  • HMR: 4-8s -> ~50ms (changes feel instant)
  • Memory usage: 1.8GB -> 400MB (significant for developer machines)

Production Builds:

  • Build time: 3m 20s -> 22s (CI pipeline runs are noticeably faster)
  • Bundle size: 2.1MB -> 1.4MB (better tree-shaking with Rollup)
  • Lighthouse score: 72 -> 89 (mostly from smaller bundles and better code splitting)

Developer Satisfaction: We ran a survey before and after. The results were unanimous — every developer on the team reported a meaningfully better development experience. The instant HMR alone changed how people worked; developers started making smaller, more frequent changes because the feedback loop was so fast.

Migration Timeline

For a typical medium-sized React application:

  • Day 1: Install Vite, create config, move index.html, update env vars
  • Day 2: Fix asset handling, SVG imports, path aliases
  • Day 3: Migrate test setup, fix any failing tests
  • Day 4: Update CI/CD pipeline, test production build
  • Day 5: QA, fix edge cases, deploy to staging

For larger applications with heavy webpack customization, add another week for handling custom plugins and configurations.

Wrapping Up

Migrating from CRA to Vite is one of the most impactful improvements you can make to your frontend development workflow. The migration itself is straightforward — most of the work is mechanical find-and-replace operations. The payoff is immediate: faster builds, happier developers, and a modern toolchain that will serve you well for years.

If you're putting off this migration because it seems risky, start with a proof-of-concept branch. Get the dev server running with Vite and see the speed difference for yourself. Once you experience sub-second hot reloads, you won't want to go back.

Vite
Create React App
migration
build tools
React
performance
DX

Related Articles

Get started

Ready to Level Up Your Product?

I take on a handful of companies at a time. Reach out to discuss your challenges and see if there's a fit.

Send a Message