
CRA to Next.js: Unlock 5x Performance & Perfect SEO (2025 Migration)
Table Of Content
- The Strategic Imperative: Why I Migrate from CRA in 2025
- Pre-Migration Blueprint: How I Audit and Plan a Transition
- My Definitive Step-by-Step Migration to the Next.js App Router
- Step 1: Project Setup & Dependency Overhaul
- Step 2: Establishing the App Router Foundation
- Step 3: Migrating Routing: From react-router-dom to next/navigation
- Table 2: react-router-dom vs. next/navigation API Mapping
- Step 4: Embracing Server Components for Data Fetching
- Step 5: Handling Interactivity with Client Components
- Step 6: Modernizing Data Mutations with Server Actions
- Step 7: Advanced Migrations: State Management and Authentication
- Post-Migration: Optimization, Tooling, and Real-World Gains
The Strategic Imperative: Why I Migrate from CRA in 2025
Introduction: The End of an Era for Create React App
For years, I used Create React App (CRA) for almost every React project. It offered a simple setup. This let me build components instead of fighting with Webpack. But the web has changed, and my methods have too. CRA was deprecated in 2024. The React team now suggests using full frameworks for production work.
This change shows CRA’s limitations. Modern applications need better performance and a richer user experience. CRA builds Client-Side Rendered (CSR) Single-Page Applications (SPAs). I found this model creates problems that modern frameworks solve.
To help you start faster, I created a GitHub migration template. This repository has the recommended App Router structure and best-practice configurations I use. Fork it to begin your migration.
Here are the top three reasons I no longer use CRA in 2025:
-
Performance Issues: CRA's Webpack setup leads to slow development servers. Hot Module Replacement (HMR) is sluggish. Production build times are long. Newer tools like Vite and Turbopack are much faster.
-
Design Limits: The CSR model slows down initial page loads. A browser must download and run a large JavaScript file before it can show content. This creates "request waterfalls." These hurt my Core Web Vitals and frustrate users.
-
Stagnant Features: CRA does not have built-in support for modern tools. These include Server-Side Rendering (SSR), Static Site Generation (SSG), and API routes. Adding these features required complex work. This work defeated CRA’s main benefit of simplicity.
The Unavoidable Rise of the Full-Stack React Framework
Next.js is the next logical step for building React applications. I no longer see a choice between "React vs. Next.js." The real question is how I can best use React inside a framework like Next.js. I migrate from CRA to Next.js to fix CRA's weaknesses. The main benefits are part of Next.js's design:
-
Better Performance: I get automatic code splitting for each page. I also get an optimized Image component (
next/image
). The framework offers flexible rendering options like SSR, SSG, and Incremental Static Regeneration (ISR). These features create faster load times. DoorDash cut its Largest Contentful Paint (LCP) by about 65% after moving to Next.js. -
Better SEO: SSR and SSG are great for search engine optimization. A CRA application gives search crawlers an almost empty HTML file. With Next.js, I can deliver fully-formed HTML content. This content is easy for crawlers to index. It has improved my sites' search rankings.
-
Better Developer Experience: My development workflow is simpler with Next.js's built-in tools. File-based routing removes the need for manual router setup. API routes let me create a backend inside the same project. This integrated system reduces the extra code and dependency problems I faced in CRA projects. This gives a huge boost to my personal productivity.
I see this migration as a business decision. Faster load times can reduce bounce rates. Better SEO can drive organic traffic. An improved developer experience helps me ship features faster.
A Glimpse into the Future: The 2025 Next.js Paradigm
The best reason for me to migrate in 2025 is the Next.js App Router. It has changed how I build React applications.
-
React Server Components (RSC): This is the main feature of the new model. RSCs run only on the server. I can use them to get data from a database or API. I can also access server resources securely. They send zero JavaScript to the client for non-interactive parts. This makes the client-side file much smaller.
-
The App Router: The App Router is built with RSCs. It allows for new patterns like nested layouts that keep their state during navigation. I can also stream user interface elements from the server. This improves how fast the page feels to the user.
-
Server Actions: This is my new way to handle data changes. I can define a function with
'use server'
and pass it to a form. I do not need a separate API endpoint. This puts my logic and my component in the same place. -
Turbopack: Turbopack is written in Rust. It is the replacement for Webpack in Next.js. It gives me a very fast development server startup and Hot Module Replacement (HMR). This fixes a major problem I had with CRA.
Table 1: CRA vs. Next.js in 2025: A Head-to-Head Comparison
Feature/Capability | Create React App (Status in 2025) | Next.js (App Router) | The "Why It Matters" Insight |
---|---|---|---|
Project Status | Deprecated, unmaintained | Actively developed, industry standard | Aligns my projects with future-proof technology and a growing ecosystem. |
Rendering Strategy | Client-Side Rendering (CSR) only | Hybrid: SSR, SSG, ISR, CSR, RSCs | Flexible rendering improves performance, UX, and SEO. RSCs dramatically reduce client-side JS. |
Routing | Requires external library (react-router-dom ) | Built-in, file-system-based App Router | Reduces my boilerplate, simplifies route management, and enables advanced layout patterns. |
Data Fetching | Client-side useEffect hooks, leading to waterfalls | async /await in Server Components, eliminating waterfalls | I get faster data loading, improved performance, and simpler, more readable code. |
SEO | Poor by default; requires pre-rendering services | Excellent by default due to server-rendering | My projects achieve higher search engine rankings and increased organic traffic. |
API Endpoints | Requires a separate backend server | Built-in API Routes and Server Actions | I can now do full-stack development within a single project, simplifying the architecture. |
Build Tooling | Webpack (slow, complex configuration) | Turbopack (Rust-based, extremely fast) | I have a much better developer experience with near-instant build times and HMR. |
Image Optimization | Manual or third-party libraries | Built-in, automatic optimization with next/image | I get faster page loads and better Core Web Vitals with little effort. |
Stop Scrolling, Start Achieving: Get Actionable Tech & Productivity Insights.
Join the inner circle receiving proven tactics for mastering technology, amplifying productivity, and finding deep focus. Delivered straight to your inbox – no fluff, just results.
Pre-Migration Blueprint: How I Audit and Plan a Transition
A good migration starts with good planning. I learned that changing code without a clear plan can cause problems and delays. In this section, I will share how I audit a CRA project. This process reduces risk. It also helps me find hidden problems in the code. I can fix these problems during the move. This turns a technical task into a strategic improvement.
My Pre-Flight Checklist
This checklist is the migration roadmap I follow. Each item is a key part of a CRA application that I check before writing any Next.js code.
1. Codebase & Dependency Audit
-
Task: I list all dependencies in
package.json
. I look for packages specific to the CRA environment, likereact-scripts
. I also look for packages that use client-side browser APIs. -
Action Plan:
-
I always uninstall the
react-scripts
package. -
I find any libraries that use the
window
orlocalStorage
objects. These do not work with server-rendering by default. I must wrap these components in a Client Component file with the'use client'
directive. I can also use Next.js's dynamic imports with SSR turned off ({ ssr: false }
). -
If I "ejected" from CRA or used tools like
craco
, those changes will not transfer. I document them and plan to copy their function in thenext.config.mjs
file.
-
2. Routing Map
-
Task: I document the application's routes from the
react-router-dom
setup. This includes static routes, dynamic routes, and protected routes. -
Action Plan: I create a map from the React Router setup to the Next.js App Router's file structure. This map guides my project file changes.
-
A route like
<Route path="/about"... />
becomes a file atapp/about/page.tsx
. -
A dynamic route like
<Route path="/blog/:slug"... />
becomesapp/blog/[slug]/page.tsx
.
-
3. Data Fetching Inventory
-
Task: I search the code for components that get data in a
useEffect
hook. This is a common pattern in CRA apps. -
Action Plan: I classify each data fetch. This tells me how to rewrite it in Next.js.
-
Static Data: Data that is fetched once and changes little. This is a good fit for Next.js's default static rendering. The fetch moves into an
async
Server Component. -
Dynamic Data: Data that must be new for every request. It is fetched in a dynamically rendered Server Component.
-
Client-Side Data: Data fetching started by a user action. The logic stays in a Client Component. The initial page can still be server-rendered.
-
4. Environment Variable Conversion Plan
-
Task: I find all environment variables in
.env
files that start withREACT_APP_
. -
Action Plan: I plan to rename these variables for Next.js. This adds a layer of security.
-
For variables needed in the browser, I change the prefix from
REACT_APP_
toNEXT_PUBLIC_
. -
For variables that should only be on the server, I remove the prefix. These are available in Server Components but not on the client. This is more secure than the CRA model.
-
5. Styling Strategy
-
Task: I find the main styling method in the application. This could be global CSS, CSS Modules, or a library like Tailwind CSS.
-
Action Plan:
-
Global CSS: I import the main stylesheet into the root
app/layout.tsx
file to apply the styles everywhere. -
CSS Modules: Next.js supports CSS Modules. Files must follow the
[name].module.css
name format. I can often use existing files with few changes. -
CSS-in-JS: This needs careful planning. The App Router renders components on the server. CSS-in-JS libraries need a special setup to pull styles on the server and add them to the first HTML file. Without this, I see a "flash of unstyled content" and other errors. My plan includes a client-side "Style Registry" component.
-
6. Public Assets and index.html
-
Task: I check the contents of the
public
directory and thepublic/index.html
file. -
Action Plan:
-
Static Assets: All static assets like images and fonts can move to the Next.js
public
folder. They will be available from the root of the application. -
index.html
: This file is no longer needed in Next.js. I have to move its contents:-
The
<html>
and<body>
tags now go in the rootapp/layout.tsx
file. -
Metadata tags like
<title>
and<meta>
should move to the Next.js Metadata API. I do this by exporting ametadata
object from my root layout and pages for better control over SEO.
-
-
How I Explain the Migration to a Team/Client
To get support for a migration, I explain its technical benefits as business value. I present the project as a strategic investment. An incremental migration, delivering value in phases, is often a good plan because it reduces risk and allows for continuous work.
Here are the points I use to make my case:
-
"We will improve our Core Web Vitals. Google uses these as a ranking factor. This investment in performance is an investment in SEO."
-
"Our application will feel much faster to our users. Reducing load times can lower bounce rates and improve conversions." (I often cite real results, like the 70% reduction in load times from some studies).
-
"This migration will improve our team's productivity. Next.js automates complex tasks. Our engineers can focus on building features, not on maintaining tools."
-
"Moving to Next.js makes our application ready for the future. We are following the official direction of the React ecosystem. This will make it easier to hire developers and stay current."
My Definitive Step-by-Step Migration to the Next.js App Router
This section is the technical core of the guide. I give a detailed, code-focused walkthrough of the migration. My goal is to show you what I do and explain why each step is needed. This will help you understand the change from a client-focused CRA structure to the server-first model of the Next.js App Router.
Step 1: Project Setup & Dependency Overhaul
The first step I take is changing the project's foundation from CRA to Next.js.
-
Uninstall CRA Scripts and Install Next.js: I open the terminal in the project root and run these commands. This removes the CRA build system and installs Next.js.
npm uninstall react-scripts npm install next@latest react@latest react-dom@latest
-
Update
package.json
Scripts: I replace thescripts
section inpackage.json
with the standard Next.js commands. This lets me run the development server and create production builds."scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }
-
Create Configuration Files: Next.js uses
next.config.mjs
for configuration. It works well with TypeScript throughtsconfig.json
.-
I create a
next.config.mjs
file in my project root. It can be minimal at first./** @type {import('next').NextConfig} */ const nextConfig = {}; export default nextConfig;
-
I create a
tsconfig.json
file. If I runnpm run dev
, Next.js will create a basic one for me.{ "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }
-
Step 2: Establishing the App Router Foundation
With the new dependencies, I can set up the App Router.
-
Create the
app
Directory: Inside thesrc
folder, I create a new directory namedapp
. This is where all routes, layouts, and pages for the App Router go. -
Create the Root Layout: This file is required. I create
app/layout.tsx
. This component wraps every page. It replaces the function of CRA'spublic/index.html
.// src/app/layout.tsx import type { Metadata } from 'next'; import './globals.css'; // Your global stylesheet export const metadata: Metadata = { title: 'My Migrated Next.js App', description: 'A new app, now with superpowers!', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); }
Pro Tip: Migrating Context Providers I learned that if a CRA app used React Context providers, they cannot be in the server-rendered
RootLayout
. They must move to a dedicated Client Component. This is a key pattern for the App Router.I create a new file
app/providers.tsx
:
// src/app/providers.tsx
'use client';
import { ThemeProvider } from 'your-theme-library';
// import other providers
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
This pattern separates client-side state logic correctly.
Step 3: Migrating Routing: From react-router-dom
to next/navigation
This step changes the routing system from a component-based one to Next.js's file-system routing.
-
Route Structure: I create folders inside
app
to match the old routes. A file namedpage.tsx
makes a route public. For example,app/dashboard/settings/page.tsx
creates the/dashboard/settings
route. Dynamic segments use bracket notation, likeapp/products/[productId]/page.tsx
. -
Migrating
<Link>
:-
Find and Replace: I globally replace
import { Link } from 'react-router-dom'
withimport Link from 'next/link'
. -
Prop Change: The
to
prop must change tohref
.
Before (React Router):
import { Link } from 'react-router-dom'; <Link to="/about">About Us</Link>
After (Next.js):
import Link from 'next/link'; <Link href="/about">About Us</Link>
-
-
Programmatic Navigation: To navigate with code, I must replace the
useNavigate
hook withuseRouter
fromnext/navigation
. This hook can only be used in components with the'use client'
directive.Before (React Router):
import { useNavigate } from 'react-router-dom'; function MyComponent() { const navigate = useNavigate(); const handleSubmit = () => { navigate('/dashboard'); }; }
After (Next.js):
'use client'; // This is now a Client Component import { useRouter } from 'next/navigation'; function MyComponent() { const router = useRouter(); const handleSubmit = () => { router.push('/dashboard'); }; }
For server-side redirects, I use the
redirect
function fromnext/navigation
.
Table 2: react-router-dom
vs. next/navigation
API Mapping
react-router-dom API | next/navigation Equivalent | Usage Context & Notes |
---|---|---|
<Link to="/path"> | <Link href="/path"> | Client or Server Component. next/link automatically prefetches routes. |
useNavigate() | useRouter() | Client Component only ('use client' ). I use router.push() , router.replace() , router.back() . |
useParams() | params prop in page / useParams() hook | In a dynamic route page (app/blog/[slug]/page.tsx ), the slug is passed as a prop. In client components, I use the useParams() hook. |
useLocation() | usePathname() | Client Component only ('use client' ). Returns the current URL's pathname. |
<Outlet /> | {children} prop in layout.tsx | The children prop in a layout.tsx file renders the content of a child layout or page. |
Step 4: Embracing Server Components for Data Fetching
This is the biggest design change I make. I move data-fetching logic from client-side useEffect
hooks to async
Server Components. This removes client-server request waterfalls and improves initial load performance.
Before: Typical CRA Data Fetching
// src/components/ProductList.js
import React, { useState, useEffect } from 'react';
function ProductList() {
const [products, setProducts] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/products')
.then((res) => res.json())
.then((data) => {
setProducts(data);
setIsLoading(false);
});
}, []);
if (isLoading) return <p>Loading...</p>;
//...
}
After: My Next.js App Router Data Fetching
// src/app/products/page.tsx
// This function runs on the server
async function getProducts() {
// Next.js extends fetch() for automatic caching.
const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
if (!res.ok) throw new Error('Failed to fetch products');
return res.json();
}
// This is a React Server Component
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
The ProductsPage
is now an async
function. It fetches data directly on the server before rendering. There are no loading states for the initial render. The page arrives at the browser with data already included.
Step 5: Handling Interactivity with Client Components
Any component that needs interactivity must be marked as a Client Component. If it uses useState
, useEffect
, or browser-only APIs like onClick
, it must be in a file with the 'use client'
directive at the top.
// src/components/CounterButton.tsx
'use client';
import { useState } from 'react';
export default function CounterButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
You clicked {count} times
</button>
);
}
A key pattern I follow is to keep Client Components as small as possible. I can pass Server Components as children
to Client Components. This lets me build interactive wrappers without pulling their static content into the client-side JavaScript file.
Step 6: Modernizing Data Mutations with Server Actions
Server Actions have changed how I handle forms. They replace the old pattern of a client-side handler making a POST
request to an API endpoint. I define an async
function with 'use server'
and pass it to a form's action
prop.
Example: My Form with a Server Action and Validation
// src/app/guestbook/page.tsx
import { revalidatePath } from 'next/cache';
import { z } from 'zod'; // A popular validation library
import { db } from '@/lib/db'; // Assume a database utility
const messageSchema = z.object({
message: z.string().min(5),
});
export default async function GuestbookPage() {
const messages = await db.getMessages();
async function createMessage(formData: FormData) {
'use server';
const result = messageSchema.safeParse({ message: formData.get('message') });
if (!result.success) {
// Handle validation errors
return;
}
await db.addMessage(result.data.message);
// Invalidate the cache and trigger a re-render with fresh data
revalidatePath('/guestbook');
}
return (
<div>
<form action={createMessage}>
<input type="text" name="message" placeholder="Your message..." />
<button type="submit">Sign Guestbook</button>
</form>
<ul>
{messages.map((msg) => <li key={msg.id}>{msg.text}</li>)}
</ul>
</div>
);
}
This single component displays data, submits a form, validates input on the server, and refreshes the UI. It does this without client-side JavaScript for the form logic. I find this pattern also works great for file uploads.
Step 7: Advanced Migrations: State Management and Authentication
State Management (Redux/Zustand)
Global client-side state libraries like Redux cannot be used directly in Server Components. The recommended pattern is to initialize the store on the client. You can hydrate it with initial state passed down from the server.
-
I create my store as usual.
-
I provide the store in a Client Component (
'use client'
). -
I fetch initial data in a Server Component and pass it as props to my client-side provider.
This pattern keeps a clear separation between server data fetching and client-side state management.
Authentication & Private Routes
The best way I have found to handle authentication is with Next.js Middleware. It runs on the server before a request is completed. This lets me protect routes efficiently.
Example: How I Protect a Dashboard with Middleware I create a middleware.ts
file at the root of my project (or inside src
).
// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const sessionToken = request.cookies.get('auth-token')?.value;
if (!sessionToken && request.nextUrl.pathname.startsWith('/dashboard')) {
const loginUrl = new URL('/login', request.url);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: '/dashboard/:path*',
};
This middleware protects all routes under /dashboard
. It redirects unauthenticated users to /login
before any page rendering happens. This is more secure and performant than client-side logic. I have found libraries like NextAuth.js are fully compatible with this pattern.
Post-Migration: Optimization, Tooling, and Real-World Gains
Finishing the code migration is a big step. The final phase for me is to check the benefits, adjust performance, and set up a modern workflow. This is where I see the full return on my migration effort.
Common Pitfalls & Debugging ("Gotchas")
Here are some common issues I have run into and how I fix them:
Hydration Errors: The error
Hydration failed...
usually means I am accessing browser-only APIs (window
,localStorage
) on the server. My Solution: I move the logic into auseEffect
hook or make sure the component is a Client Component ('use client'
).
'use client'
Errors: The error "You're importing a component that needsuseState
..." means I need to add the'use client'
directive to the top of that component's file.Server Action UI Not Updating: If a form submission does not update the UI, I probably forgot to call
revalidatePath('/your-path')
after the data change.
Unlocking Performance Gains: The Payoff
The main reason I migrate is often for performance. I can measure these gains myself using Google Lighthouse.
Table 3: Performance Impact: Before & After My Migration
Metric | Before (Typical CRA App) | After (My Optimized Next.js App) | Improvement | What This Means for Users |
---|---|---|---|---|
Lighthouse Performance Score | 60-75 | 95-100 | +30-60% | A much faster, smoother overall experience. |
Largest Contentful Paint (LCP) | 3.5s - 4.5s | 1.5s - 2.2s | ~50-65% | Main content appears much faster, reducing perceived load time. |
Interaction to Next Paint (INP) | 350ms - 500ms | 150ms - 200ms | ~50-60% | The UI responds almost instantly to user clicks and inputs. |
Initial JS Payload (gzipped) | 300KB - 600KB | 80KB - 150KB | ~60-75% | Far less JavaScript for the browser to download and execute. |
HMR Time (Dev Experience) | 1.3s - 2.5s | 100ms - 200ms | ~90% | My development changes appear almost instantly, boosting productivity. |
Enabling Turbopack for Blazing-Fast Development
For a better development experience, I turn on Turbopack. It is the Rust-based replacement for Webpack. My Action: I run the dev server with the --turbo
flag.
npm run dev -- --turbo
Turbopack is stable for next dev
and fixes a big problem with CRA's tooling.
My Essential Tooling for a Modern Workflow
Automating the Tedious with Codemods
Codemods are scripts that change code automatically. They save me from a lot of manual changes. Next.js provides official codemods to help with migrations.
How I Use Them:
npx @next/codemod <transform> <path>
Key Codemods I Use for Migration:
-
next-image-to-legacy-image
&next-image-experimental
: These help upgrade my<Image>
components. -
App Router Codemods: A set of codemods can automate parts of the migration. These include
replace-next-router
(for routing hooks) andreplace-next-head
(for the Metadata API).
Linters and Formatters
I maintain code quality with eslint-config-next
. It includes rules for Next.js best practices. I pair it with Prettier for a consistent code style.
Final Thoughts: My Life After Migration
My migration to Next.js is the start of a new phase for my application.
What I Monitor Post-Deployment:
-
Performance: I track Core Web Vitals using Vercel Analytics or Google Search Console. I set performance budgets to stop regressions.
-
SEO: I monitor indexing status and search rankings in Google Search Console.
-
Error Tracking: I use a service like Sentry or LogRocket to monitor both client and server-side errors.
With my application on a modern base, I can explore advanced App Router features. These include Streaming UI with loading.tsx
, Parallel Routes, and advanced caching. To plan my next steps, I often refer to a complete frontend roadmap.
To help you start faster, I created a GitHub migration template. This repository has the recommended App Router structure and best-practice configurations I use. Fork it to begin your migration.