<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/how-to-migrate-create-react-app-to-vite -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/how-to-migrate-create-react-app-to-vite/raw.md -->
<!-- Source path: content/guides/how-to-migrate-create-react-app-to-vite.mdx -->

---
og_image: "/images/guides/how-to-migrate-create-react-app-to-vite.webp"
title: "How to Migrate from Create React App to Vite 2026"
description: "The definitive CRA to Vite migration guide for 2026. Every step, every gotcha, and how to handle the edge cases that trip up most migrations for 2026."
date: "2026-03-08"
author: "PkgPulse Team"
tags: ["vite", "create-react-app", "migration", "react", "typescript", "2026"]
featured_comparison: "vite-vs-webpack"
tier: 1
---

## TL;DR

**CRA to Vite migration takes 30-60 minutes and gives you a 40x faster dev server.** Create React App was deprecated in 2023 and should not be used for new projects or maintained indefinitely. The migration is well-documented and mechanical: remove `react-scripts`, install Vite, move `index.html`, rename env vars from `REACT_APP_` to `VITE_`, update imports from `process.env` to `import.meta.env`. Most apps migrate with under 20 file changes.

## Key Takeaways

- **Dev server: 8,000ms → 200ms** — CRA bundles everything; Vite serves ESM natively
- **5 main changes**: `react-scripts` → `vite`, index.html location, env vars, tsconfig, scripts
- **Tests**: CRA used Jest; switch to Vitest (see Jest → Vitest guide) or keep Jest standalone
- **CRA was deprecated 2023** — no security patches on CRA vulnerabilities
- **TypeScript CRA**: same migration, just add TypeScript-specific steps

---

## Prerequisites and Assessment

Before you start, spend a few minutes auditing your current project. This assessment prevents surprises halfway through the migration.

**Check your CRA version and scripts.** Open `package.json` and look at the `react-scripts` version and the scripts block. CRA projects typically look like this:

```json
{
  "dependencies": {
    "react-scripts": "5.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
}
```

Note that `react-scripts` 5.x ships Webpack 5, Babel 7, and ESLint 8 under the hood. All of this gets replaced by Vite's ESBuild-based pipeline. This is the reason the dev server becomes 40x faster — Vite uses native ES modules in development and never bundles during dev, while CRA recompiles the entire dependency graph on every change.

**Why you should migrate now.** Create React App was formally deprecated by the React team in March 2023. The `react-scripts` package has not received security updates since then. The npm audit for a fresh CRA project in 2026 shows dozens of high-severity vulnerabilities. Vite, by contrast, releases regularly and powers the scaffolding for React, Vue, Svelte, and most modern frameworks.

**Inventory your dependencies.** Check for these CRA-specific things that need attention:

- **`process.env.REACT_APP_*` env vars** — these need renaming to `VITE_*`
- **SVG imports as React components** — CRA had built-in support; Vite needs a plugin
- **`public/index.html`** — this file moves to the project root in Vite
- **`%PUBLIC_URL%` template strings** — these get stripped out
- **`require()` calls** — CommonJS `require()` doesn't work in Vite's ESM environment without configuration
- **Jest tests** — CRA wired up Jest automatically; Vite does not. You'll need to set up Vitest or configure Jest standalone.

With a list of these items in hand, you can estimate your migration time. A typical CRA project with standard setup finishes in under an hour. Projects that rely heavily on custom Webpack configuration (via `craco` or `react-app-rewired`) may take longer because you'll need to translate those overrides into Vite plugins.

---

## Step 1: Remove react-scripts and Install Vite

Start by uninstalling the CRA toolchain entirely. There is no partial migration — `react-scripts` and Vite cannot coexist as your primary build tool.

```bash
# Remove CRA
npm uninstall react-scripts

# Verify CRA is gone
grep "react-scripts" package.json  # Should show nothing
```

Now install Vite and the React plugin:

```bash
npm install -D vite @vitejs/plugin-react

# For TypeScript projects (most CRA projects are TypeScript):
npm install -D vite @vitejs/plugin-react typescript

# Recommended extras:
npm install -D vite-tsconfig-paths   # Reads path aliases from tsconfig
npm install -D vite-plugin-svgr      # SVG as React components (if you use SVG imports)
```

The `@vitejs/plugin-react` plugin provides Fast Refresh (HMR), JSX transformation, and Babel integration for advanced transforms. `vite-tsconfig-paths` automatically picks up `paths` aliases from your `tsconfig.json` so you don't need to duplicate them in `vite.config.ts`.

Update your `package.json` scripts:

```json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest run"
  }
}
```

The `tsc &&` prefix on the build command runs the TypeScript compiler to type-check before building. Vite itself does not type-check — it strips types without checking them, leaving that to `tsc`.

---

## Step 2: Create vite.config.ts

Create `vite.config.ts` at the project root. This is the complete configuration for a CRA-equivalent setup:

```typescript
// vite.config.ts — at project root
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import svgr from 'vite-plugin-svgr';  // Only if you import SVGs as React components

export default defineConfig({
  plugins: [
    react(),
    tsconfigPaths(),   // Handles paths from tsconfig.json
    svgr(),            // If you use: import { ReactComponent as Logo } from './logo.svg'
  ],
  resolve: {
    alias: {
      '@': '/src',  // Optional: manual alias if not using vite-tsconfig-paths
    },
  },
  server: {
    port: 3000,       // Match CRA's default port
    open: true,       // Auto-open browser on dev start
    proxy: {
      // Proxy API calls to avoid CORS in development
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'build',   // CRA used 'build'; Vite defaults to 'dist' — match CRA
    sourcemap: true,
  },
});
```

Key decisions to understand in this config:

- **`server.port: 3000`** — CRA defaults to port 3000. Vite defaults to 5173. Set this explicitly so you don't break saved browser bookmarks or OAuth redirect URIs during development.
- **`build.outDir: 'build'`** — CRA outputs to `build/`. Vite defaults to `dist/`. If your deployment pipeline references `build/` (common in Netlify, Vercel, and CI scripts), keep this set to `'build'`.
- **`proxy`** — CRA's `package.json` `"proxy"` field had built-in proxy support. Vite's equivalent is `server.proxy` in `vite.config.ts`.

---

## Step 3: Move index.html to the Project Root

CRA keeps `index.html` in the `public/` directory. Vite expects it at the project root. This is the most structurally different change in the migration.

```bash
mv public/index.html ./index.html
```

After moving, edit `index.html` to make two changes: remove `%PUBLIC_URL%` prefixes and add the Vite module script entry point.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- Before: href="%PUBLIC_URL%/favicon.ico" -->
    <!-- After: root-relative path, no variable -->
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <!-- Before: href="%PUBLIC_URL%/manifest.json" -->
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!-- ADD THIS — Vite requires an explicit entry point script tag -->
    <!-- CRA injected this automatically; Vite requires it explicitly -->
    <script type="module" src="/src/index.tsx"></script>
    <!-- Use .tsx for TypeScript, .jsx for JavaScript -->
  </body>
</html>
```

The two critical changes are:
1. All `%PUBLIC_URL%` occurrences become simple root-relative paths like `/favicon.ico`. Search and replace this across the entire file.
2. The `<script type="module" src="/src/index.tsx">` tag is essential. Without it, Vite has no entry point and the page will be blank.

Other files in `public/` (images, icons, `manifest.json`, `robots.txt`) stay in `public/` and Vite copies them to the output directory automatically.

---

## Step 4: Rename Environment Variables

CRA required the `REACT_APP_` prefix for custom environment variables. Vite uses `VITE_`. Any variable that does not have this prefix is not exposed to the browser bundle, which is a security feature — it prevents accidentally leaking server secrets.

Update your `.env` files:

```bash
# Before (.env, .env.local, .env.development, .env.production):
REACT_APP_API_URL=https://api.example.com
REACT_APP_GOOGLE_MAPS_KEY=abc123
REACT_APP_STRIPE_PUBLIC_KEY=pk_live_...

# After:
VITE_API_URL=https://api.example.com
VITE_GOOGLE_MAPS_KEY=abc123
VITE_STRIPE_PUBLIC_KEY=pk_live_...
```

Update all usages in your source code. The access pattern also changes — from `process.env` to `import.meta.env`:

```typescript
// Before (CRA):
const apiUrl = process.env.REACT_APP_API_URL;
const mapsKey = process.env.REACT_APP_GOOGLE_MAPS_KEY;

// Check runtime environment:
if (process.env.NODE_ENV === 'development') {
  console.log('dev mode');
}
if (process.env.NODE_ENV === 'production') {
  // do something
}

// After (Vite):
const apiUrl = import.meta.env.VITE_API_URL;
const mapsKey = import.meta.env.VITE_GOOGLE_MAPS_KEY;

// Vite provides boolean flags instead of the string 'development':
if (import.meta.env.DEV) {
  console.log('dev mode');
}
if (import.meta.env.PROD) {
  // do something
}

// Mode is still available as a string if you need it:
if (import.meta.env.MODE === 'staging') {
  // custom mode
}
```

For TypeScript, add a `src/vite-env.d.ts` file to get proper type checking on your env vars:

```typescript
// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_GOOGLE_MAPS_KEY: string;
  readonly VITE_STRIPE_PUBLIC_KEY: string;
  // Add all your VITE_ variables here for full type safety
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

This gives you autocomplete and type errors when you mistype an env variable name.

---

## Step 5: Update tsconfig.json

CRA's `tsconfig.json` contains settings that are incompatible with Vite's build process. Replace it with a Vite-optimized configuration:

```json
// tsconfig.json — Vite-compatible
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    // Key change: "bundler" resolution is Vite-native
    // CRA used "node" or "node16" — change this
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,

    // Vite handles transpilation; tsc only type-checks
    "noEmit": true,
    "jsx": "react-jsx",

    // Strict settings (keep from CRA):
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    // Add Vite's client types for import.meta.env
    "types": ["vite/client"]
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
```

Also create `tsconfig.node.json` for the Vite config file itself:

```json
// tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}
```

The critical setting change is `"moduleResolution": "bundler"`. CRA used `"node"` resolution which is CommonJS-oriented. `"bundler"` is designed for tools like Vite that use native ESM and allows importing TypeScript files with their `.ts` extensions explicitly.

---

## Step 6: Handle SVG Imports

CRA had a built-in Webpack loader that let you import SVG files as React components using a named `ReactComponent` export. This is not a Web standard — it was CRA magic. Vite does not include this by default.

Install `vite-plugin-svgr` if you haven't already:

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

With the plugin configured in `vite.config.ts` (shown in Step 2), your SVG imports need a small syntax change:

```tsx
// CRA: SVG as React component using named export
import { ReactComponent as Logo } from './assets/logo.svg';

// Vite with vite-plugin-svgr: use the ?react query parameter
import Logo from './assets/logo.svg?react';

// Usage is identical:
function Header() {
  return <Logo className="h-8 w-8" />;
}
```

If you just want to use an SVG as an image URL (not as a React component), Vite handles that natively without any plugin:

```tsx
// Import SVG as a URL string — works in Vite without any plugin
import logoUrl from './assets/logo.svg';

function Header() {
  return <img src={logoUrl} alt="Logo" className="h-8 w-8" />;
}
```

Use the `?react` pattern only when you need to manipulate the SVG's internals (changing colors via CSS, animating paths). For static logos and icons, the plain import is simpler.

---

## Common Pitfalls

These are the issues that catch developers during migration and are not obvious from the documentation.

**`require()` doesn't work.** Vite runs in native ESM mode. CommonJS `require()` calls will throw a `ReferenceError: require is not defined` at runtime in the browser. Convert all `require()` to `import`:

```typescript
// Before:
const config = require('./config.json');
const lodash = require('lodash');

// After:
import config from './config.json';
import lodash from 'lodash';
```

Node.js built-ins (`require('path')`, `require('fs')`) are server-side only and cannot run in the browser regardless of the bundler.

**`process.env` is undefined.** Vite does not polyfill `process.env` globally. Only `import.meta.env` works. If you use a library that internally references `process.env.NODE_ENV`, Vite will inline the value at build time — but your own code must use `import.meta.env`.

**Jest tests break.** CRA configured Jest with a custom transformer that handled JSX and TypeScript. When you remove `react-scripts`, Jest loses its configuration. You have two options: migrate to Vitest (the Vite-native test runner with a Jest-compatible API), or configure Jest manually with `babel-jest` or `ts-jest`. The Vitest migration is usually straightforward and worth doing at the same time.

**Absolute imports stop working.** If you had `"paths": {"@/*": ["src/*"]}` in `tsconfig.json`, those work at type-check time but Vite needs to know about them separately. The `vite-tsconfig-paths` plugin reads your `tsconfig.json` paths and makes them work in Vite's bundler. Add it to `vite.config.ts` plugins as shown in Step 2.

**`homepage` in package.json.** CRA read the `homepage` field to set the base URL for production builds. Vite uses `base` in `vite.config.ts`:

```typescript
// vite.config.ts
export default defineConfig({
  base: '/my-app/',  // Replaces "homepage": "/my-app/" in CRA's package.json
});
```

---

## Verification

After completing all steps, run these checks to confirm a successful migration:

```bash
# Start the dev server
npm run dev
# Should start in under 500ms at http://localhost:3000

# Run type checking
npx tsc --noEmit
# Should show no TypeScript errors

# Build for production
npm run build
# Should complete, output in /build directory

# Preview the production build locally
npm run preview
# Should serve from /build at http://localhost:4173

# Check for CRA remnants
grep -r "react-scripts\|REACT_APP_\|process\.env\.NODE_ENV\|%PUBLIC_URL%" src/
# Should return no matches
```

Check the terminal output carefully on `npm run build`. Vite reports bundle size by chunk. If you see chunks over 500KB, consider adding code splitting with dynamic `import()` to lazy-load heavy routes.

---

## Understanding the Performance Difference

The 40x startup time improvement is not marketing — it reflects a fundamental architectural change in how development servers work.

CRA wraps Webpack, which is a module bundler. In development mode, Webpack processes every file in your `src/` directory, resolves all imports, applies all loaders (Babel for JSX/TypeScript, CSS modules, SVG loaders), and bundles everything into a single JavaScript file before serving the first request. For a medium-sized app with 200 components, this initial bundle compilation takes 5-15 seconds. Every time you change a file, Webpack recompiles the entire affected bundle.

Vite takes a different approach. In development, it starts an HTTP server and serves files individually using native browser ES modules. When the browser requests `http://localhost:3000`, Vite serves `index.html`. The browser parses the HTML, finds `<script type="module" src="/src/index.tsx">`, and requests that file. Vite transforms only that file (TypeScript stripping + JSX transform using ESBuild, which is 10-100x faster than Babel), and serves it. The browser then parses the imports in that file and requests each dependency. Vite transforms each one on demand.

The result: first meaningful render appears in under 500ms regardless of project size, because Vite only processes what the browser actually requests. A 500-component app starts just as fast as a 10-component app, because only the components visible in the current route get processed initially.

Hot module replacement (HMR) is similarly faster. When you save a file, Vite only transforms that one file and sends the update to the browser. The browser replaces just that module without a full reload. CRA's HMR recompiles the entire bundle chunk containing the changed file, which can still take 1-2 seconds in a large app.

For production builds, both CRA and Vite create optimized bundles. Vite uses Rollup for production (not ESBuild), which produces highly optimized code with excellent tree-shaking. The production bundle sizes are comparable between CRA and Vite, but Vite's build is typically faster.

---

## Handling Tests After Migration

CRA configured Jest with a custom transformer that handled JSX and TypeScript automatically. When you remove `react-scripts`, Jest no longer knows how to parse `.tsx` files. You have two paths forward.

**Option A: Migrate to Vitest.** Vitest is Vite's test runner. It uses the same configuration and plugins as Vite, so it works out of the box with your `vite.config.ts`. The API is Jest-compatible, meaning `describe`, `test`, `expect`, `vi.fn()` (instead of `jest.fn()`), and `vi.mock()` (instead of `jest.mock()`) — most tests migrate with a global find-and-replace. Install it with `npm install -D vitest @vitest/ui jsdom`, add `"test": "vitest"` to your scripts, and add a test config to `vite.config.ts`:

```typescript
// vite.config.ts — add test block
export default defineConfig({
  // ... existing config
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts',
  },
});
```

**Option B: Keep Jest standalone.** If you have a large Jest test suite and want to avoid migrating at the same time as the Vite migration, configure Jest independently. Install `@babel/preset-react`, `@babel/preset-typescript`, `babel-jest`, and create a `babel.config.js`. This is more setup work but keeps your tests untouched.

Most teams find the Vitest migration straightforward enough to do at the same time as the Vite migration, since the API compatibility is high.

---

## When to Choose Vite vs Other Alternatives

If your CRA project is growing into a larger application and you are evaluating your options beyond just "migrate to Vite," here is a quick comparison:

| Scenario | Best Choice |
|----------|-------------|
| Standard React SPA migration from CRA | Vite |
| Full-stack with API routes and SSR | Next.js |
| Need Turbopack's incremental compilation for very large apps | Next.js 15+ |
| Monorepo with shared packages | Vite + Turborepo |
| Electron desktop app | Vite (excellent Electron support) |

For most CRA migrations, Vite is the correct destination. It is the officially recommended replacement and what the React team now points to in their documentation.

---

## Further Reading

- [Turbopack vs Vite — which bundler wins in 2026?](/guides/turbopack-vs-vite-2026)
- [Vite package health, download trends, and changelog](/packages/vite)
- [How to migrate from Webpack to Vite](/guides/how-to-migrate-webpack-to-vite-2026)
