Next.js vs Create React App: 334 Prerendered Pages vs One index.html
Measured on this storefront: 334 HTML files from 27 route files, and 68 of 92 components that never reach the browser. Plus where a CRA-shaped app still wins.
Create React App builds a single-page app: one index.html, one JavaScript bundle, and every route resolved in the browser after that bundle runs. Next.js can do that too, but it can also render a route ahead of time into real HTML. For anything with public URLs that matters — which is the whole argument, and it is measurable.
Here it is measured. npm run build on this storefront (Next.js 16.2.9, React 19.2.4) prerenders 546 routes — 336 HTML files and 204 generated Open Graph images — from 27 page.tsx files and 3 generateStaticParams functions. The equivalent CRA build emits one index.html and asks Google to run your JavaScript 336 times.
The state of Create React App
React's own Creating a React App page now sends you to a framework rather than to CRA. That has been the direction of travel for a while, and the practical consequence is what you should weigh: a new CRA project starts on a tool the docs no longer point at, so "which do I pick" is mostly settled before the technical comparison begins.
The technical comparison is still worth having, because why it is settled tells you what you actually gain — and there are real cases below where a CRA-shaped app is still the right answer.
What the two actually differ on
| Create React App | Next.js (App Router) | |
|---|---|---|
| Build output | One index.html + JS bundles | HTML per route, or server-rendered on demand |
| Routing | Client-side, a library you add (react-router) | Filesystem — a folder per route segment |
| First paint without JS | Empty <div id="root"> | The rendered page |
Per-page <title>/meta | Set at runtime by a JS side-effect | generateMetadata, in the HTML as shipped |
| Components sent to the browser | All of them | Only the ones marked "use client" |
| Server code | Separate service | Route handlers, Server Actions, middleware |
| Images | Whatever you wire up | next/image, or build-time variants (see below) |
| Config | react-scripts, eject to change | next.config.ts |
The row that everything else follows from is the third one. A CRA page's initial HTML contains no content. Every crawler, link preview, and reader on a slow connection sees an empty shell until the bundle downloads, parses and executes.
What 336 HTML files cost to produce
Three functions. This is all of the one that generates 111 product pages:
// src/app/templates/[slug]/page.tsx
export function generateStaticParams(): Params[] {
return catalog.map((p) => ({ slug: p.slug }));
}
catalog is a typed array in src/data/catalog.ts. Next.js calls this at build time, gets the list of slugs, and renders /templates/<slug> once per entry into static HTML. Add a product to the array and the next build has one more page — no route registration, no sitemap edit.
Per-page metadata is a sibling function on the same route, so the title, description, canonical and OG image are in the HTML as shipped rather than assigned by a useEffect after hydration:
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
const product = getProduct(slug);
if (!product) return { title: "Template not found" };
return {
title: { absolute: productTitle(product) },
description: productMetaDescription(product),
alternates: { canonical: `/templates/${slug}` },
robots:
product.status === "available"
? undefined
: { index: false, follow: true },
// …openGraph
};
}
Note the robots line. A product that is not yet purchasable returns index: false, follow: true from a data field — per-page indexing control derived from the catalog, in the served HTML. In a CRA app that is a <meta> tag written by JavaScript, which is exactly the thing you cannot rely on a crawler to have executed.
The same mechanism generates the 204 blog OG cards: src/app/blog/[slug]/opengraph-image.tsx carries its own generateStaticParams — without it the file renders per request instead of at build time, which is a cheap mistake to make and an expensive one to leave running.
68 of 92 components never reach the browser
This is the difference a comparison table understates. In src/components there are 92 component files. Exactly 24 carry "use client":
molecules/ AuthCard, BlogCtaLink, BuyButton, ContactForm, DownloadMenu,
EditionPicker, FaqItem, NewsletterForm, PreviewModal,
ProductDownloadGroup, PurchaseCta, RedemptionPicker, RefundButton,
SavedTemplates, SettingsForm, TemplateCard, TemplateGallery,
UseCaseCard, WishlistButton
organisms/ DashboardTabs, Header, SavedTemplatesGrid, TemplatesExplorer,
YourProductsGrid
Every one of them owns interaction state: an accordion that opens, a carousel that slides, a form that submits, a menu that toggles. The other 68 components are Server Components — they run at build time, emit HTML, and ship no JavaScript at all. Under CRA all 92 would be in the bundle, because in a client-side SPA there is nowhere else for a component to run.
That is the concrete meaning of "Next.js is faster". Not a benchmark: a smaller set of files with any reason to be in the bundle.
The CSS story is separate and worth stating because it is the same for both tools — one 79,444-byte stylesheet covers the entire site, because Tailwind's output is a function of the classes you used, not of the framework you used them in.
What Next.js also hands you that CRA leaves as homework
- Middleware.
src/proxy.tsruns on every request — here, to refresh the Supabase auth session. In CRA there is no request to hook; you need a separate server. - Server-side secrets. A route handler (
src/app/api/webhooks/lemonsqueezy/) verifies a signed webhook with a key that never enters the bundle. Every environment variable a CRA app reads is in the JavaScript it ships. sitemap.ts/robots.ts. Real files that emit real routes, with per-URL dates computed from data.- Streaming and code splitting per route, rather than per-import-you-remembered-to-lazy.
Where the CRA shape is still right
Being honest about this is the useful part of the comparison:
- An app behind a login with no public URLs. An internal dashboard has no SEO to gain and no crawler to satisfy. The prerendering argument evaporates.
- A widget or embed that mounts into someone else's page. You want a bundle, not a site.
- You need a different backend anyway. If the server is a Django or Rails app you are not replacing, a plain SPA plus that API is a simpler topology than a second server-rendering runtime in front of it.
In those cases the modern replacement is Vite with the React plugin rather than CRA, but the architecture CRA gave you is still a reasonable one. Just choose it deliberately.
And one real cost of the Next.js side, from this codebase: we deliberately do not use next/image. Every <img> points at a WebP derivative pre-built by npx tsx scripts/images/build-variants.ts and resolved through src/lib/imageVariants.ts — a 1060px card variant and a 1600px detail variant. That keeps a request-time image optimizer (and its bill) out of the picture entirely. The framework's defaults are not always the right answer; they are just a good place to start arguing from.
If you are migrating
| CRA thing | Next.js App Router equivalent |
|---|---|
src/index.js + ReactDOM.createRoot | src/app/layout.tsx |
<BrowserRouter> routes | Folders under src/app, one per segment |
<Route path="/x/:id"> | src/app/x/[id]/page.tsx |
react-helmet / document.title | export const metadata or generateMetadata |
process.env.REACT_APP_* | NEXT_PUBLIC_* for the browser; bare names stay server-only |
public/index.html | The root layout.tsx — there is no HTML template to edit |
A component using useState | The same component, plus "use client" at the top |
npm run eject | next.config.ts |
The migration order that causes the least pain: move the routes first with everything marked "use client", confirm it builds, then delete that directive file by file, starting with the leaves. Each one you remove is code that stops shipping.
Mistakes and how they show up
| Symptom | Cause | Fix |
|---|---|---|
useState is not defined in a server file | The component runs on the server by default | Add "use client" — but only to the component that needs it, not its parent |
window is not defined at build | Browser-only code running during prerender | Guard in useEffect, or move it into a client component |
Page renders, but its <title> never updates | Setting the title imperatively | Export metadata/generateMetadata from the route |
| A dynamic route 404s in production | No generateStaticParams for that segment | Add it, returning every valid param |
| OG image regenerates on every request | opengraph-image.tsx without its own generateStaticParams | Give it one — it is a separate route |
| The whole page is in the bundle anyway | "use client" on a layout or a page | Push it down to the interactive leaf |
NEXT_PUBLIC_ secret leaked | The prefix means "inline into the browser bundle" | Drop the prefix; read it in a server component or route handler |
Frequently asked questions
Is Create React App still usable in 2026? It still builds. But React's own guidance now points at frameworks, so a new CRA project starts on a path the docs have moved away from — and if you want the SPA architecture without the framework, Vite is the tool people actually reach for.
Is Next.js slower to build than CRA? It does more work — this build renders 336 pages and 204 images rather than bundling one entry point. What you save is at request time, and on 336 URLs that trade is not close. (It is also faster than it used to be: Turbopack cut this build from 42s to 27s.)
Can I use Next.js as a pure SPA?
Largely, yes: put "use client" at the top of your pages and you have a client-rendered app with filesystem routing. You will be paying for a framework whose main advantage you switched off, which is the argument for Vite instead.
Does Next.js require a Node server? Not for a site like this one. Everything measured above is static output — HTML, images and assets that any CDN can serve. The server only comes in for the dynamic routes: the auth pages, the dashboard, the webhook and the download endpoint.
Do I need next/image?
No. Build-time variants plus a plain <img> is a legitimate alternative when your images are known at build time, and it is what this site ships. Reach for next/image when the images are user-supplied or remote.
Templates in this post
ASoc Zenith is a growth-marketing studio site — a metrics hero, a six-service grid and four detailed case studies — the kind of content-heavy marketing page where prerendered HTML is the entire point. ASoc Aegis is a risk-management/GRC landing template built around a Risk Center dashboard preview and a three-tier comparison pricing table, and ASoc Ally markets an AI support chatbot with a live widget preview, an eight-feature grid and two-tier pricing.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
