Skip to main content
ASoc
Comparison

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.

The ASoc Team10 min read

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 AppNext.js (App Router)
Build outputOne index.html + JS bundlesHTML per route, or server-rendered on demand
RoutingClient-side, a library you add (react-router)Filesystem — a folder per route segment
First paint without JSEmpty <div id="root">The rendered page
Per-page <title>/metaSet at runtime by a JS side-effectgenerateMetadata, in the HTML as shipped
Components sent to the browserAll of themOnly the ones marked "use client"
Server codeSeparate serviceRoute handlers, Server Actions, middleware
ImagesWhatever you wire upnext/image, or build-time variants (see below)
Configreact-scripts, eject to changenext.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.ts runs 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 thingNext.js App Router equivalent
src/index.js + ReactDOM.createRootsrc/app/layout.tsx
<BrowserRouter> routesFolders under src/app, one per segment
<Route path="/x/:id">src/app/x/[id]/page.tsx
react-helmet / document.titleexport const metadata or generateMetadata
process.env.REACT_APP_*NEXT_PUBLIC_* for the browser; bare names stay server-only
public/index.htmlThe root layout.tsx — there is no HTML template to edit
A component using useStateThe same component, plus "use client" at the top
npm run ejectnext.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

SymptomCauseFix
useState is not defined in a server fileThe component runs on the server by defaultAdd "use client" — but only to the component that needs it, not its parent
window is not defined at buildBrowser-only code running during prerenderGuard in useEffect, or move it into a client component
Page renders, but its <title> never updatesSetting the title imperativelyExport metadata/generateMetadata from the route
A dynamic route 404s in productionNo generateStaticParams for that segmentAdd it, returning every valid param
OG image regenerates on every requestopengraph-image.tsx without its own generateStaticParamsGive it one — it is a separate route
The whole page is in the bundle anyway"use client" on a layout or a pagePush it down to the interactive leaf
NEXT_PUBLIC_ secret leakedThe 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.

Keep reading

Comparison9 min read

Next.js vs. Nuxt: 24 of 92 Components Opt Into the Client

Not React versus Vue — the real split is where each framework lets you draw the server/client boundary. Next.js draws it through the import graph; Nuxt's default is universal.

Read more
Comparison11 min read

Next.js vs React + Vite for Admin Dashboards: How to Choose

Both ship excellent dashboards. The decision comes down to where your data lives, whether you need SEO, and who deploys it — not to raw performance.

Read more