Migrating from Create React App to Vite in 2026

Create React App had a good run. When Facebook released it in 2016, it solved a real problem: bootstrapping a React project with Webpack, Babel, and a sensible default configuration was genuinely painful, and CRA abstracted all of that away behind a single CLI command. For several years it was the right default for new projects.

In 2022, the React team officially acknowledged in their documentation that CRA was not the recommended starting point for new applications. By 2024, the npm package had gone more than two years without a meaningful release. Security vulnerabilities in the dependency tree accumulated without patches. In 2026, using CRA for a new project is difficult to justify on any grounds — and if you have an existing CRA application, the migration to Vite is now well-understood and routinely accomplishes in a single afternoon for medium-sized apps.

This guide covers exactly what changes between CRA and Vite, what breaks during migration, and how to verify the migration is complete before merging to production.

Why CRA Is Dead in 2026

CRA's architecture is built on Webpack 4/5 and Babel. Webpack bundles everything — every import in your application is resolved, transpiled, and bundled together before the development server can start. For a project with 150 components, this cold-start bundling can take 30–90 seconds. Every time a file changes, Webpack re-evaluates the dependency graph and re-bundles affected modules, which is why HMR in CRA projects routinely takes 3–10 seconds even for trivial edits.

Vite's architecture is fundamentally different. In development mode, Vite does not bundle at all. It serves files as native ES modules directly to the browser, letting the browser's own module system handle dependency resolution. Only files actually requested by the browser are transformed, and only on demand. This is why Vite's cold-start time is measured in milliseconds rather than seconds regardless of project size, and why HMR updates are consistently under 50ms.

For production builds, Vite uses Rollup (not esbuild) to produce a well-optimised bundle with code splitting, tree-shaking, and asset hashing. Dependency pre-bundling during development uses esbuild, which is 10–100x faster than Babel for the pre-bundling step.

The practical difference: a developer working on a CRA application with 200 components wastes roughly 15–30 minutes per day waiting for HMR updates. A team of five loses an hour of productive development time every single day. This compounds into thousands of hours annually across the Indian developer ecosystem, where CRA adoption remains high due to the volume of older tutorials that still reference it.

Step 1: Install Vite and Update Dependencies

In your existing CRA project, first remove CRA's dependency and install Vite's React plugin:

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

If you are using TypeScript (and you should be), add the Vite TypeScript types:

npm install --save-dev @types/node

You do not need to install @vitejs/plugin-react-swc separately unless you specifically want SWC instead of Babel for the transform step. The default @vitejs/plugin-react uses Babel, which is compatible with most CRA-era Babel plugins and presets your project may be relying on.

Step 2: Create vite.config.ts

Create vite.config.ts at the project root. This replaces the hidden Webpack configuration that CRA managed internally.

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

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Replicate CRA's absolute import support
      // if you used MODULE_DIRECTORIES or jsconfig paths
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    port: 3000, // Keep the same port as CRA for muscle memory
    open: true,
  },
  build: {
    outDir: "build", // CRA outputs to 'build', Vite defaults to 'dist'
    sourcemap: true,
  },
  // Handle Node.js globals that CRA polyfilled automatically
  define: {
    global: "globalThis",
  },
});

Setting outDir: "build" keeps your deployment scripts compatible — CRA outputs to build/ and Vite defaults to dist/. Either change your deployment scripts or align Vite to match CRA's output path.

Step 3: Move index.html to the Project Root

This is the change that trips up most CRA-to-Vite migrations. CRA puts index.html inside the public/ folder and injects the script tags automatically during the build. Vite treats index.html as an entry point and expects it at the project root.

Move the file:

mv public/index.html ./index.html

Then update the moved index.html to reference your entry point explicitly. CRA injected the script tag at build time; Vite expects it in the HTML source:

<!-- Remove any CRA-specific %PUBLIC_URL% references -->
<!-- Change: -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
<!-- To: -->
<link rel="icon" href="/favicon.ico">

<!-- Add this inside <body> — the Vite entry point reference -->
<script type="module" src="/src/index.tsx"></script>

Any other %PUBLIC_URL% occurrences in index.html should be replaced with / or removed. Vite serves the public/ directory at the root path automatically, so /favicon.ico correctly resolves to public/favicon.ico.

Step 4: Update Environment Variable Prefixes

CRA requires environment variables intended for browser use to be prefixed with REACT_APP_. Vite uses VITE_. Any other prefix is not exposed to the client bundle — this is a security feature that prevents accidental exposure of server-side secrets.

Rename all relevant variables in your .env files:

# Before (CRA)
REACT_APP_API_URL=https://api.example.com
REACT_APP_RAZORPAY_KEY=rzp_live_xxxxxxxx

# After (Vite)
VITE_API_URL=https://api.example.com
VITE_RAZORPAY_KEY=rzp_live_xxxxxxxx

Then update every reference in your source code. CRA variables were accessed via process.env.REACT_APP_*; Vite uses import.meta.env.VITE_*:

// Before (CRA)
const apiUrl = process.env.REACT_APP_API_URL;
const nodeEnv = process.env.NODE_ENV;

// After (Vite)
const apiUrl = import.meta.env.VITE_API_URL;
const nodeEnv = import.meta.env.MODE; // 'development' | 'production'

If you use process.env.NODE_ENV in many places, the define block in vite.config.ts can shim it:

define: {
  "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  global: "globalThis",
},

Step 5: Fix SVG Imports and Module Resolution Differences

CRA configured Webpack to allow importing SVG files as React components via a custom loader. In a CRA project, this works:

import { ReactComponent as Logo } from "./logo.svg";

Vite does not support this syntax out of the box. You have two options.

Option A: Install vite-plugin-svgr and add it to your Vite config. This provides identical behaviour to CRA's SVG component transform:

npm install --save-dev vite-plugin-svgr
// vite.config.ts
import svgr from "vite-plugin-svgr";

export default defineConfig({
  plugins: [
    react(),
    svgr(), // Enables: import { ReactComponent as Logo } from "./logo.svg"
  ],
});

Option B: Convert SVG imports to use Vite's native URL import syntax and inline SVGs in JSX manually. This is more work upfront but reduces a plugin dependency:

// Native Vite SVG (as URL)
import logoUrl from "./logo.svg";
<img src={logoUrl} alt="Logo" />

Step 6: Update package.json Scripts

Replace the CRA react-scripts commands with Vite equivalents:

{
  "scripts": {
    "start": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "test": "vitest"
  }
}

Note that vite preview is the replacement for serve — it starts a local server on the production build output for pre-deployment testing. This is distinct from the development server (vite) which serves files as native ES modules.

Step 7: Migrate Tests from Jest to Vitest

CRA's test runner is Jest configured to use Babel for transpilation. Jest does not understand native ES modules, which is exactly what Vite produces. The cleanest migration path is switching to Vitest, which reuses vite.config.ts and has a Jest-compatible API.

npm install --save-dev vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom

Add the Vitest configuration inside vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,       // No need to import describe/it/expect
    environment: "jsdom",
    setupFiles: ["./src/setupTests.ts"],
    css: true,
  },
});

Update src/setupTests.ts to import from @testing-library/jest-dom/vitest:

import "@testing-library/jest-dom/vitest";

Most test files need no changes — describe, it, expect, beforeEach and the Testing Library API are all identical. The only common difference is mock syntax: jest.fn() becomes vi.fn(), jest.mock() becomes vi.mock(). A simple find-and-replace handles this across the test suite.

Frequently Asked Questions

Is it worth migrating a large CRA app to Vite?

Yes, for the developer experience gains alone. A CRA app with 300+ components typically spends 45–90 seconds on cold development start and 5–15 seconds per HMR update. After migrating to Vite, cold starts drop to 2–8 seconds and HMR is consistently under 50ms. For a team of five developers, this recaptures 30–60 minutes of productive development time per developer per day. The migration itself takes 2–8 hours for a typical medium-sized app, making the return on investment immediate. CRA receives no security patches; Vite is actively maintained and releases regularly.

Does migrating from CRA to Vite break existing tests?

CRA tests run on Jest with jsdom, which cannot handle native ES modules. When you migrate to Vite, switching to Vitest is the cleanest path — it shares your vite.config.ts, understands ES modules natively, and has a Jest-compatible API where the only recurring change is replacing jest.* with vi.*. If you must keep Jest, configure babel-jest with @babel/preset-env targeting CommonJS modules. It works, but it adds configuration maintenance overhead and defeats part of the purpose of moving to Vite.

Should I migrate to Vite or just move to Next.js instead?

If your app is a dashboard, admin panel, internal tool, or anything behind authentication where SEO does not matter — migrate to Vite. You keep your existing SPA routing and deployment model (a static folder on any CDN or even an S3 bucket). If your app has public-facing pages that need to rank in search results, or if you need server-side data fetching for performance on slow mobile connections (common in tier-2 India), moving to Next.js is justified. The Next.js migration is significantly more work — a complete rewrite of routing and data-fetching logic — versus the Vite migration which touches only configuration and tooling, not your component code.