Migrating from Create React App to Next.js
A practical look at migrating a production React app from Create React App to Next.js. We cover what worked, what didn't, and whether we recommend it.


Background
We've been building React applications since 2019. For most of that time, Create React App (CRA) was the default choice for bootstrapping projects. It worked, but it came with friction: slow development builds, limited configuration control, and no built-in routing or SSR.
When Next.js matured with the App Router in version 13+, we were skeptical at first. Another framework migration? But after seeing the performance gains and DX improvements firsthand in client projects, we decided to migrate our own studio site and a few production client applications.
The Migration Process
Our migration took place over three weeks, affecting three production applications. Here's what we learned.
Bundle Size: The Immediate Win
This was the first thing we noticed. CRA ships a heavy runtime and all dependencies bundled together. Next.js uses tree-shaking and automatic code splitting by route.
// Before: CRA, entire bundle loaded upfront
// Initial JS: ~320KB gzipped
// All routes, all components, all at once
// After: Next.js, route-based code splitting
// Home page: ~45KB gzipped
// Dashboard page: ~60KB gzipped (loaded on demand)
// Admin panel: ~35KB gzipped (loaded on demand)For our applications, this translated to an ~65% reduction in initial JavaScript payload. The difference was especially noticeable on mobile connections.
File-Based Routing: A Game Changer
CRA left routing entirely to the developer. Every project had a different setup like React Router v5, v6, or a custom solution. Next.js gave us predictable file-based routing that every developer on our team understands instantly.
// CRA: manual routing setup
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/dashboard/settings" element={<Settings />} />
<Route path="/blog/:slug" element={<BlogPost />} />
</Routes>
</BrowserRouter>
)
}
// Next.js: file-based routing
// app/page.tsx → /
// app/dashboard/page.tsx → /dashboard
// app/dashboard/settings/page.tsx → /dashboard/settings
// app/blog/[slug]/page.tsx → /blog/:slugServer Components: Doing More on the Server
The biggest paradigm shift was embracing React Server Components. What used to require client-side data fetching, loading spinners, and useEffect chains can now be a simple async component.
// CRA: client-side data fetching
function BlogPage() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <Spinner />
return <PostList posts={posts} />
}
// Next.js: server component, no client JS needed
async function BlogPage() {
const posts = await fetchPosts() // Runs on server
return <PostList posts={posts} /> // Zero client JS
}What Was Tricky
No migration is painless. Here were our friction points:
- Window references: Any code referencing
window,document, orlocalStorageneeds to be wrapped in client components or useEffect. - Third-party libraries: Some npm packages assume a browser environment. We had to audit each one and add "use client" boundaries or find alternatives.
- CSS-in-JS: Emotion and styled-components required extra configuration. We recommend Tailwind CSS for new Next.js projects.
- Image migration: CRA's
public/folder approach doesn't leverage optimization. Moving to the Next.js Image component required path changes.
Performance Improvements
After migrating, we measured these improvements across our applications:
- LCP improved by 40% (from 3.2s to 1.9s)
- Initial bundle size dropped from ~320KB to ~85KB
- Time to Interactive reduced by 55%
- SEO traffic increased 60% thanks to server-side rendering
Would We Recommend It?
Yes. If you're starting a new React project, use Next.js. If you're maintaining a CRA project, start planning your migration now. CRA is officially in maintenance mode.
The performance improvements alone justify the switch. The developer experience with file-based routing, server components, and automatic code splitting makes your team more productive. Combined with better SEO and a more maintainable codebase, it's not even close.
Migration Checklist
If you're planning a migration:
- Set aside dedicated time (estimate 2-3 weeks for a medium app)
- Start with a fresh Next.js project and move code over incrementally
- Use the incremental adoption guide so you can keep existing pages while migrating
- Audit all third-party dependencies for browser-only assumptions
- Switch to Tailwind CSS if you're using CSS-in-JS
- Configure the Next.js Image component early because it's not a drop-in replacement
Building something great? Let's talk →