Introduction
Choosing the right React framework can make or break your project before a single line of production code ships. Next.js, Gatsby, and Create React App each represent fundamentally different philosophies about how web applications should be built, rendered, and deployed. In 2020, this debate reached a fever pitch as the React ecosystem matured and developers discovered that the default choice was no longer sufficient for production-grade applications.
Understanding the differences between these frameworks requires looking beyond surface-level feature comparisons. Each one makes deliberate trade-offs in rendering strategy, build performance, developer experience, and deployment flexibility. This guide breaks down those trade-offs so you can choose the right tool for your specific use case, whether you are building a static marketing site, a dynamic SaaS dashboard, or a hybrid application that needs both.
By the end of this article, you will understand the architectural underpinnings of each framework, when to reach for each one, and how to migrate between them when your project requirements evolve.
Understanding the Three Frameworks: Core Concepts
Create React App: The Starting Point
Create React App was Facebook answer to the configuration fatigue that plagued React developers in 2016. Before CRA, setting up a React project meant wrestling with webpack configurations, Babel presets, ESLint rules, and testing setups. CRA abstracted all of that behind a single command: npx create-react-app my-app.
CRA is fundamentally a client-side rendering framework. When a user visits your application, the browser downloads an empty HTML shell, then a JavaScript bundle, and React takes over to render the entire UI in the browser. This approach is simple and works well for applications behind authentication, where SEO is irrelevant and the initial blank screen is acceptable because users expect a brief loading state.
The architecture is deliberately opinionated and locked down. CRA uses webpack under the hood but does not expose the configuration unless you eject with npm run eject, which is a one-way operation. This design decision keeps projects consistent but limits customization. You cannot easily add custom webpack plugins, change the Babel configuration, or modify the dev server without ejecting or using tools like react-app-rewired.
Gatsby: The Static Site Generator
Gatsby took a different approach by betting heavily on static site generation. At build time, Gatsby queries data from any source using GraphQL, then generates static HTML files for every page. These files are served directly from a CDN, resulting in blazing-fast load times and excellent SEO.
Gatsby plugin ecosystem is its greatest strength. With over 2,500 plugins, you can connect to virtually any data source: WordPress, Contentful, Sanity, Markdown files, REST APIs, and more. The GraphQL data layer unifies these sources into a single queryable schema, which is powerful but introduces complexity. Every data transformation requires understanding Gatsby node APIs, GraphQL queries, and the plugin lifecycle.
The trade-off is build time. As your site grows to thousands of pages, Gatsby build process can become painfully slow. Incremental builds and Deferred Static Generation have improved this, but for very large sites, the build step remains a bottleneck.
Next.js: The Full-Stack Framework
Next.js, built by Vercel, is the most flexible of the three. It supports server-side rendering, static site generation, incremental static regeneration, and client-side rendering often within the same application. This flexibility makes Next.js the Swiss Army knife of React frameworks.
With the introduction of the App Router in Next.js 13, the framework pushed further into full-stack territory with React Server Components, streaming SSR, and nested layouts. The Pages Router, which was the standard before Next.js 13, is still fully supported and widely used.
Next.js handles routing through the file system: files in the pages/ or app/ directory automatically become routes. API routes allow you to build backend endpoints without a separate server. Image optimization, font optimization, and script optimization are built in.
Architecture and Design Patterns
Rendering Strategies Compared
The most fundamental difference between these three frameworks is how they deliver content to the browser.
Create React App uses client-side rendering exclusively. The server sends a minimal HTML document with a root div, and React mounts the application in the browser. This means the user sees a blank screen or a loading spinner until the JavaScript bundle downloads and executes.
// CRA entry point - everything happens client-side
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);Gatsby generates static HTML at build time. When a user visits a page, they receive fully rendered HTML immediately, then React hydrates it to add interactivity. This gives you the best of both worlds for content-heavy sites: fast initial paint and full interactivity after hydration.
// Gatsby page component with GraphQL data
import React from 'react';
import { graphql } from 'gatsby';
export const query = graphql`
query BlogPost($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
date
}
}
}
`;
const BlogPost = ({ data }) => (
<article>
<h1>{data.markdownRemark.frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.markdownRemark.html }} />
</article>
);
export default BlogPost;Next.js lets you choose per page. A marketing landing page can be statically generated, a dashboard can be server-rendered, and a settings page can be client-rendered.
// Next.js page with multiple rendering options
// Option 1: Static Generation
export async function getStaticProps() {
const posts = await fetchPosts();
return { props: { posts }, revalidate: 60 };
}
// Option 2: Server-Side Rendering
export async function getServerSideProps(context) {
const session = await getSession(context.req);
if (!session) return { redirect: '/login' };
return { props: { user: session.user } };
}Data Fetching Patterns
Each framework handles data differently, and this is where architectural decisions have the most impact.
CRA has no built-in data fetching pattern. You are free to use fetch, Axios, React Query, SWR, or any other library. This flexibility is liberating for experienced developers but confusing for beginners who need to assemble their own data layer.
Gatsby mandates GraphQL as the data layer. All data must pass through Gatsby GraphQL schema. This creates a unified API but adds complexity and a learning curve. The gatsby-node.js file is where you create pages programmatically and transform data.
Next.js provides built-in data fetching functions. You can use any data fetching library alongside these primitives, including server components that fetch data directly in the App Router.
Routing Architecture
CRA relies on React Router for client-side routing. You define routes manually in your application code using BrowserRouter, Routes, and Route components.
Gatsby uses file-based routing through the src/pages/ directory. Dynamic routes require the createPages API in gatsby-node.js.
Next.js pioneered file-system routing in the React ecosystem. Every file in pages/ or app/ becomes a route automatically. Dynamic segments use bracket syntax: pages/blog/[slug].tsx maps to /blog/:slug.
Step-by-Step Implementation
Setting Up Each Framework
Let us build the same simple blog with each framework to illustrate the practical differences.
Create React App requires manual setup for routing and data fetching:
npx create-react-app my-blog --template typescript
cd my-blog
npm install react-router-dom// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Post from './pages/Post';
import Layout from './components/Layout';
function App() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/post/:id" element={<Post />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
export default App;Gatsby setup involves plugin configuration:
npm init gatsby my-blog
cd my-blog
npm install gatsby-plugin-mdx @mdx-js/react// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'posts',
path: `${__dirname}/content/posts`,
},
},
'gatsby-plugin-mdx',
],
};// src/pages/index.tsx
import React from 'react';
import { graphql, Link } from 'gatsby';
export const query = graphql`
query AllPosts {
allMdx(sort: { frontmatter: { date: DESC } }) {
nodes {
id
frontmatter { title, date, description }
fields { slug }
}
}
}
`;
export default function Home({ data }) {
return (
<div>
<h1>Blog</h1>
{data.allMdx.nodes.map(post => (
<article key={post.id}>
<h2><Link to={post.fields.slug}>{post.frontmatter.title}</Link></h2>
<p>{post.frontmatter.description}</p>
</article>
))}
</div>
);
}Next.js provides the most streamlined setup:
npx create-next-app@latest my-blog --typescript --app
cd my-blog// app/page.tsx
import Link from 'next/link';
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
});
return res.json();
}
export default async function Home() {
const posts = await getPosts();
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2><Link href={`/post/${post.id}`}>{post.title}</Link></h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}Real-World Use Cases
Use Case 1: Marketing Landing Page
A marketing site with 50 pages, SEO requirements, and a headless CMS is Gatsby sweet spot. The build-time GraphQL layer connects to Contentful or WordPress, generates static HTML, and deploys to a CDN. Every page loads instantly because there is no runtime server involved.
However, Next.js with SSG achieves the same result with less configuration overhead. If your team does not want to learn Gatsby GraphQL layer, Next.js getStaticProps with generateStaticParams is a simpler alternative.
Use Case 2: SaaS Dashboard
A SaaS application behind authentication, with dynamic data, real-time updates, and no SEO requirements, is the worst fit for Gatsby. The data changes constantly, so static generation is pointless. CRA works here, but you lose SSR for the initial load.
Next.js is ideal because you can server-render the shell, fetch user-specific data on the server, and use client components for interactive elements. API routes eliminate the need for a separate backend for simple operations.
Use Case 3: E-Commerce Store
An e-commerce site needs both static product pages for SEO and performance and dynamic features like cart management and user accounts. This is a hybrid use case that requires SSR for some pages and CSR for others.
Next.js handles this with ISR for product pages. They are generated at build time but can be revalidated in the background. Cart and checkout pages use client-side rendering or SSR as needed.
Use Case 4: Documentation Site
A documentation site with versioning, search, and markdown content is a strong use case for Gatsby. The gatsby-plugin-mdx and gatsby-source-filesystem plugins handle markdown processing elegantly. The search functionality can be implemented with Algolia Gatsby plugin.
Best Practices for Production
-
Choose based on rendering needs: If every page can be static, use Gatsby or Next.js SSG. If you need per-page rendering flexibility, use Next.js. If you need no SSR at all, CRA is fine for prototyping.
-
Consider build times: Gatsby build times grow linearly with page count. For sites with 10,000 or more pages, Next.js with ISR is more practical because it defers generation to runtime.
-
Evaluate the data layer: If you are comfortable with GraphQL and want a unified data layer, Gatsby architecture is powerful. If you prefer direct data fetching, Next.js or CRA give you more control.
-
Plan for scale: CRA apps do not scale well for SEO-dependent projects. If there is any chance your project will need SSR or SSG in the future, start with Next.js.
-
Factor in deployment: Gatsby deploys as static files anywhere that serves HTML works. Next.js requires a Node.js runtime for SSR and API routes. CRA deploys as static files like Gatsby.
-
Assess team expertise: Gatsby requires GraphQL knowledge. Next.js requires understanding SSR and SSG concepts. CRA requires assembling your own architecture. Match the framework to your team skill set.
-
Monitor ecosystem health: Gatsby ecosystem has slowed significantly after Netlify acquisition. Next.js has the most active development and community momentum. CRA has been officially deprecated by the React team.
-
Use framework-specific optimizations: Next.js has
next/imagefor automatic image optimization. Gatsby hasgatsby-imagewith blur-up placeholders. CRA requires manual optimization.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Choosing Gatsby for a dynamic app | Slow builds, unnecessary complexity | Use Next.js or CRA for dynamic content |
| Using CRA for SEO-critical sites | Poor search engine visibility | Switch to Next.js or Gatsby for SSG or SSR |
| Ignoring Gatsby build time growth | 10+ minute builds for large sites | Implement incremental builds or switch to Next.js ISR |
| Ejecting CRA too early | Maintenance burden with custom webpack | Use react-app-rewired or craco for customization |
| Over-fetching data in Gatsby | Bloated GraphQL bundles | Use fragment colocation and lazy loading |
| Mixing SSR and CSR incorrectly in Next.js | Waterfall requests, poor performance | Use parallel data fetching in server components |
Performance Optimization
Bundle Size Management
All three frameworks support code splitting, but they implement it differently. CRA uses React.lazy and Suspense. Gatsby automatically code-splits per page with built-in prefetching for visible links. Next.js code-splits per route automatically and supports dynamic imports with next/dynamic.
// Next.js dynamic import for code splitting
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false,
});Image Optimization
Images are often the largest assets on a page. CRA requires manual optimization with WebP format, responsive srcset, and lazy loading. Gatsby provides gatsby-image with automatic lazy loading, blur-up placeholders, and responsive sizes. Next.js offers next/image which handles optimization, resizing, format conversion to WebP or AVIF, and lazy loading automatically.
// Next.js image optimization
import Image from 'next/image';
function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}Comparison with Alternatives
| Feature | CRA | Gatsby | Next.js |
|---|---|---|---|
| Rendering | CSR only | SSG | SSG + SSR + ISR + CSR |
| Data Fetching | Manual | GraphQL | Built-in functions |
| Routing | React Router | File-based + createPages | File-based |
| Build Time | Fast | Slow at scale | Medium |
| SEO | Poor | Excellent | Excellent |
| Deployment | Static files | Static files | Node.js runtime |
| Learning Curve | Low | Medium | Medium |
| Image Optimization | Manual | gatsby-image | next/image |
| API Routes | No | Serverless functions | Built-in |
| Active Development | Deprecated | Slowing | Very active |
Advanced Patterns and Techniques
Hybrid Rendering in Next.js
One of Next.js most powerful features is mixing rendering strategies within a single application. Product listing pages can use static generation, individual product pages can use ISR with revalidation, and cart pages can be fully dynamic with forced server rendering on every request.
// app/products/page.tsx - Static generation
export default async function ProductsPage() {
const products = await getProducts();
return <ProductList products={products} />;
}
// app/cart/page.tsx - Dynamic rendering
export const dynamic = 'force-dynamic';
export default async function CartPage() {
const cart = await getCart();
return <CartView cart={cart} />;
}Gatsby Deferred Static Generation
For large Gatsby sites, Deferred Static Generation allows you to defer non-critical pages to be generated on first request rather than at build time.
// gatsby-node.js
exports.createPages = async ({ actions }) => {
const { createPage } = actions;
const posts = await getPosts();
posts.forEach(post => {
createPage({
path: post.slug,
component: './src/templates/post.tsx',
context: { id: post.id },
defer: post.date < '2020-01-01',
});
});
};Testing Strategies
Testing approaches vary by framework. With CRA, you use standard React Testing Library with Mock Service Worker for API mocking. With Gatsby, you need to mock the GraphQL layer and use Gatsby test utilities. With Next.js, you can test server components by awaiting the component function and rendering the result.
// CRA: Testing with MSW
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import Home from './Home';
const server = setupServer(
rest.get('/api/posts', (req, res, ctx) => {
return res(ctx.json([{ id: 1, title: 'Test Post' }]));
})
);
beforeAll(() => server.listen());
afterAll(() => server.close());
test('renders posts', async () => {
render(<Home />);
await waitFor(() => {
expect(screen.getByText('Test Post')).toBeInTheDocument();
});
});Future Outlook
The React framework landscape has shifted dramatically. Create React App is officially deprecated and the React team now recommends starting with a framework like Next.js, Remix, or SvelteKit. This signals a fundamental change in how React applications should be built.
Next.js continues to push the boundaries with React Server Components, the App Router, and edge computing support. Its integration with Vercel deployment platform creates a tightly coupled but extremely polished developer experience.
Gatsby future is uncertain after Netlify acquisition. While the project is still maintained, new features have slowed, and the community has largely migrated to Next.js or newer alternatives like Astro.
The emerging trend is content layer frameworks like Astro and Contentlayer that separate content processing from rendering, potentially making the framework debate less relevant for content-heavy sites.
Conclusion
The choice between Next.js, Gatsby, and Create React App ultimately comes down to your project rendering requirements and long-term trajectory.
Use Create React App if you are building a learning project or a prototype, but be aware it is deprecated and should not be used for new production projects.
Use Gatsby if you are building a content-heavy static site and your team is comfortable with GraphQL. Its plugin ecosystem and build-time data layer are powerful for the right use case, but consider the ecosystem declining momentum.
Use Next.js for virtually everything else. Its flexibility, active development, and comprehensive feature set make it the default choice for React applications in 2020 and beyond.
Key takeaways:
- Next.js is the safest default choice for new React projects
- Gatsby excels at static content sites with complex data sources
- CRA is deprecated and you should migrate existing projects to a framework
- Rendering strategy should drive your framework choice
- Consider deployment requirements early as static versus server changes everything
- Evaluate your team expertise and willingness to learn new paradigms
Start by mapping your pages to their ideal rendering strategy. If you find you need multiple strategies in one application, Next.js is almost certainly your answer. If every page is static, Gatsby or Next.js SSG will serve you well. And if you are just starting out, reach for Next.js because it grows with your project.