Stop overthinking it. Migrating from Create React App to Vite isn't rocket science, but every tutorial makes it sound scarier than it is. I've migrated dozens of CRA projects. Here's the real process that works.
Why Vite Instead of Next.js?
Everyone assumes you should migrate CRA to Next.js. That's like replacing your sedan with a semi-truck because trucks are more powerful. If your app doesn't need SSR, routing, or API routes, Vite gives you 90% of the benefits with 10% of the complexity.
Choose Vite when:
- Your app is client-side only (SPA)
- You want faster builds but don't need framework features
- Your team wants minimal config changes
- You're not ready for SSR complexity
Choose Next.js when:
- You need SEO and server-side rendering
- You want a full-stack framework with API routes
- You're building a content-heavy site
- Your team is ready for more architectural changes
For most CRA apps, Vite is the right choice. You get instant dev server startup and build times that don't make you contemplate career changes.
The Real Migration Process (4-8 Hours)
Forget the bullshit "2-hour migration" claims in tutorials. Plan for 4-8 hours, maybe a full day if your project is complex or you hit edge cases. Here's what actually happens:
Phase 1: Backup and Prepare (15 minutes)
## Create a new branch - you WILL need to rollback at some point
git checkout -b migrate-to-vite
## Backup package.json in case you need to compare later
cp package.json package.json.backup
## Optional but smart: test your current build works
npm run build
npm run test
Don't skip the backup. Spent like 3 hours debugging why process.env.NODE_ENV
was undefined, only to realize I'd accidentally nuked the wrong line from package.json. When your build inevitably shits the bed at 11 PM and you need to diff configs to figure out what the hell you broke, you'll understand why this step matters.
Phase 2: Remove CRA Dependencies (10 minutes)
## Remove react-scripts and other CRA-specific packages
npm uninstall react-scripts
## Remove any CRA-specific eslint configs if you have them
npm uninstall @testing-library/jest-dom @testing-library/react @testing-library/user-event
Some guides tell you to remove testing libraries. Don't. Made this mistake once and spent like 2 hours reinstalling shit when tests started failing with cryptic errors like "ReferenceError: expect is not defined" or some weird Jest nonsense. You'll need them later, just different versions.
Phase 3: Install Vite Dependencies (5 minutes)
## Install Vite and plugins
npm install --save-dev vite @vitejs/plugin-react
## Install updated testing dependencies if you removed them
npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event vitest jsdom
Why vitest
? Because Jest with Vite is a fucking nightmare. Spent like 4 hours trying to make Jest play nice with Vite's ES modules, getting delightful errors like "SyntaxError: Cannot use import statement outside a module" and "ReferenceError: global is not defined" and some other cryptic bullshit. Vitest is Jest-compatible but actually works without making you question your life choices.
Phase 4: Move and Configure Files (30 minutes)
This is where tutorials get vague and you start hitting real problems.
Move your index.html:
mv public/index.html index.html
Update index.html: Remove all the CRA-specific stuff and add the Vite entry point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Your App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Rename src/index.js to src/main.jsx:
mv src/index.js src/main.jsx
Why main.jsx
? Because Vite expects JSX files to have the .jsx
extension. This will save you from cryptic build errors.
Create vite.config.js (full config reference):
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
open: true, // Auto-open browser
port: 3000, // Same port as CRA for convenience
},
build: {
outDir: 'build', // Same output dir as CRA
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
},
})
Phase 5: Fix Environment Variables (The Hidden Nightmare)
This is where most tutorials fail you. CRA uses REACT_APP_
prefixes. Vite uses VITE_
prefixes (env variables guide). You need to update EVERY environment variable.
Find all REACT_APP variables:
## Search your entire codebase for REACT_APP usage
grep -r "REACT_APP" src/
## Check your .env files
ls .env*
Update .env files:
## OLD (.env, .env.local, .env.production, etc.)
REACT_APP_API_URL=https://api.example.com
REACT_APP_VERSION=1.2.3
## NEW
VITE_API_URL=https://api.example.com
VITE_VERSION=1.2.3
Update JavaScript code:
// OLD - CRA style
const apiUrl = process.env.REACT_APP_API_URL;
// NEW - Vite style
const apiUrl = import.meta.env.VITE_API_URL;
Pro tip: Create a helper function to make the transition easier:
// src/utils/env.js
export const getEnvVar = (key) => {
// Support both CRA and Vite during migration
return import.meta.env[`VITE_${key}`] || process.env[`REACT_APP_${key}`];
};
// Usage
const apiUrl = getEnvVar('API_URL');
Phase 6: Update Package Scripts (5 minutes)
Replace CRA scripts with Vite equivalents in package.json
:
{
"scripts": {
"dev": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui"
}
}
Keep start
as an alias for dev
- muscle memory is real.
Phase 7: Test Everything (2-4 hours)
This is where you'll spend most of your time. Every project breaks differently.
## Start dev server
npm run dev
## Does it start? Does it load?
## Check browser console for errors
Common Problems You'll Actually Hit
Problem 1: Import Path Issues
Error: Failed to resolve import './Component' from 'src/App.jsx'
Fix: Vite is stricter about file extensions (unlike CRA which was weirdly forgiving):
// CRA allowed this
import Component from './Component';
// Vite needs explicit extensions
import Component from './Component.jsx';
// or
import Component from './Component.js';
Solution: Use find-and-replace to add extensions systematically:
## Find files that might need extension fixes
find src -name "*.js" -o -name "*.jsx" | xargs grep -l "import.*from ['\"].\/"
## Or just fix them as the dev server yells at you
## Usually faster than trying to be clever about it
Problem 2: Dynamic Imports Break
Error: Failed to resolve module specifier
CRA's webpack handled dynamic imports differently. With Vite:
// CRA style that breaks in Vite
const module = await import(`./components/${componentName}`);
// Vite needs explicit paths or glob imports
const modules = import.meta.glob('./components/*.jsx');
const module = await modules[`./components/${componentName}.jsx`]()
Problem 3: CSS Import Order Changes
Symptoms: Styling looks different between CRA and Vite
Fix: Vite processes CSS imports in a different order. Import your global CSS in your main.jsx
:
// src/main.jsx - import CSS at the top
import './index.css';
import './App.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
// rest of your app code
Problem 4: SVG Imports Don't Work
Error: Module "./logo.svg" does not provide an export named 'ReactComponent'
CRA automatically converted SVGs to React components. Vite doesn't:
// CRA style - breaks
import { ReactComponent as Logo } from './logo.svg';
// Vite needs a plugin or manual handling
import logoUrl from './logo.svg'; // Gets the URL
// OR install a plugin for React component support
npm install vite-plugin-svgr
Add to vite.config.js
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr'
export default defineConfig({
plugins: [react(), svgr()],
// ... rest of config
})
Problem 5: Environment Variables Still Don't Work
Symptoms: import.meta.env.VITE_API_URL
is undefined
Debug steps (learned the hard way):
- Check your
.env
file location (should be in project root, not src/) - Verify variable names start with
VITE_
(forgot this 3 times) - Restart the dev server (Vite caches env vars and won't pick up changes)
- Check if you have multiple
.env
files conflicting (.env vs .env.local vs .env.development)
## Debug env vars
npm run dev -- --debug
## Or add logging to see what's available
console.log('Available env vars:', import.meta.env);
The Nuclear Option: Start Fresh
If your migration is completely fucked (happens to the best of us), cut your losses. Fresh start:
## Create new Vite app
npm create vite@latest my-app-vite -- --template react
## Copy your src/ folder over
cp -r old-cra-app/src/* my-app-vite/src/
## Copy public assets (pray they work)
cp -r old-cra-app/public/* my-app-vite/public/
## Manually copy dependencies from old package.json
## Don't copy devDependencies - you probably don't need half of them
Sometimes starting over is faster than archaeological debugging. Spent like 6 hours trying to fix CSS modules that worked fine in CRA but broke mysteriously in Vite, only to find our component naming was causing some weird import conflicts. Could've rebuilt the whole thing in maybe 4 hours? Maybe less?
Performance Reality Check
Here's what you'll actually get after migration:
Development server:
- CRA: 15-30 seconds startup (sometimes longer if webpack is having a bad day)
- Vite: <1 second startup (seriously, it's wild)
Hot reload:
- CRA: 2-5 seconds to see changes (sometimes more if you're unlucky)
- Vite: basically instant (like under 100ms usually)
Production builds:
- CRA: 60-120 seconds (maybe more for big apps)
- Vite: 15-45 seconds depending on project size and how much stuff you have
Bundle size:
- Usually somewhere around 10-20% smaller with Vite, but depends on your project
- Could be more or less - better tree shaking and modern build optimizations help
The performance difference is dramatic enough that your team will never want to go back to CRA.
When to Bail Out
Sometimes migration isn't worth it:
- Legacy codebase with heavy CRA-specific hacks - If you ejected and customized webpack heavily, staying put might be smarter
- Team bandwidth issues - If your team is already overwhelmed debugging production issues, stability beats speed
- Complex monorepo setup - CRA in monorepos with custom configurations can be a nightmare to migrate
- Business pressure - If your PM is breathing down your neck about shipping features, don't add migration risk to the timeline
CRA still works. It's just slow and won't get new features. If that's acceptable for your project timeline, there's no shame in staying put temporarily. The key is making a conscious decision rather than procrastinating indefinitely. Set a deadline: "We migrate by Q2 2026" and stick to it. Your future self will thank you when dev servers start instantly instead of giving you time to grab coffee and question your career choices.