BeyondIT logo
BeyondIT
CRA to Next.js: Unlock 5x Performance & Perfect SEO (2025 Migration)
Technology

CRA to Next.js: Unlock 5x Performance & Perfect SEO (2025 Migration)

19 min read
#Technology
Table Of Content

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:

  1. 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.

  2. 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.

  3. 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/CapabilityCreate React App (Status in 2025)Next.js (App Router)The "Why It Matters" Insight
Project StatusDeprecated, unmaintainedActively developed, industry standardAligns my projects with future-proof technology and a growing ecosystem.
Rendering StrategyClient-Side Rendering (CSR) onlyHybrid: SSR, SSG, ISR, CSR, RSCsFlexible rendering improves performance, UX, and SEO. RSCs dramatically reduce client-side JS.
RoutingRequires external library (react-router-dom)Built-in, file-system-based App RouterReduces my boilerplate, simplifies route management, and enables advanced layout patterns.
Data FetchingClient-side useEffect hooks, leading to waterfallsasync/await in Server Components, eliminating waterfallsI get faster data loading, improved performance, and simpler, more readable code.
SEOPoor by default; requires pre-rendering servicesExcellent by default due to server-renderingMy projects achieve higher search engine rankings and increased organic traffic.
API EndpointsRequires a separate backend serverBuilt-in API Routes and Server ActionsI can now do full-stack development within a single project, simplifying the architecture.
Build ToolingWebpack (slow, complex configuration)Turbopack (Rust-based, extremely fast)I have a much better developer experience with near-instant build times and HMR.
Image OptimizationManual or third-party librariesBuilt-in, automatic optimization with next/imageI 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, like react-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 or localStorage 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 the next.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 at app/about/page.tsx.

    • A dynamic route like <Route path="/blog/:slug"... /> becomes app/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 with REACT_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_ to NEXT_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 the public/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 root app/layout.tsx file.

      • Metadata tags like <title> and <meta> should move to the Next.js Metadata API. I do this by exporting a metadata 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.

  1. 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
    
  2. Update package.json Scripts: I replace the scripts section in package.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"
    }
    
  3. Create Configuration Files: Next.js uses next.config.mjs for configuration. It works well with TypeScript through tsconfig.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 run npm 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.

  1. Create the app Directory: Inside the src folder, I create a new directory named app. This is where all routes, layouts, and pages for the App Router go.

  2. Create the Root Layout: This file is required. I create app/layout.tsx. This component wraps every page. It replaces the function of CRA's public/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 named page.tsx makes a route public. For example, app/dashboard/settings/page.tsx creates the /dashboard/settings route. Dynamic segments use bracket notation, like app/products/[productId]/page.tsx.

  • Migrating <Link>:

    • Find and Replace: I globally replace import { Link } from 'react-router-dom' with import Link from 'next/link'.

    • Prop Change: The to prop must change to href.

    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 with useRouter from next/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 from next/navigation.

Table 2: react-router-dom vs. next/navigation API Mapping

react-router-dom APInext/navigation EquivalentUsage 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() hookIn 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.tsxThe 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.

  1. I create my store as usual.

  2. I provide the store in a Client Component ('use client').

  3. 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.

The tRPC Secret to 100% Type-Safe TypeScript : Stop API Type Hell

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 a useEffect hook or make sure the component is a Client Component ('use client').

  • 'use client' Errors: The error "You're importing a component that needs useState..." 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

MetricBefore (Typical CRA App)After (My Optimized Next.js App)ImprovementWhat This Means for Users
Lighthouse Performance Score60-7595-100+30-60%A much faster, smoother overall experience.
Largest Contentful Paint (LCP)3.5s - 4.5s1.5s - 2.2s~50-65%Main content appears much faster, reducing perceived load time.
Interaction to Next Paint (INP)350ms - 500ms150ms - 200ms~50-60%The UI responds almost instantly to user clicks and inputs.
Initial JS Payload (gzipped)300KB - 600KB80KB - 150KB~60-75%Far less JavaScript for the browser to download and execute.
HMR Time (Dev Experience)1.3s - 2.5s100ms - 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) and replace-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.