Full-Stack
Next.js vs. React SPA for SEO-Driven Sites - Why I Rebuilt My Own Portfolio
The Next.js vs React SEO debate comes up in almost every project I scope: should this be a client-rendered single-page app, or a server/static-rendered framework like Next.js? I had to answer that question for my own portfolio, sameerbuilds.com. It's a React 18 + TypeScript + Vite single-page app with GSAP animations and a Three.js 3D character, served as static files by Caddy behind Cloudflare. The SPA was the right call for the interactive side, but it wasn't the right vehicle for content that needs to be indexed.
So instead of rewriting everything, I rebuilt the publishing layer: a separately generated static blog, markdown compiled to plain HTML at build time, sitting alongside the SPA. Here's how I weighed the two approaches, and where I think most teams get the decision wrong.
Why I Rebuilt My Content Layer Instead of Rewriting the App
My portfolio needed two things that pull in different directions. The homepage needed motion — timelines, scroll-triggered animations, a 3D character — and a component-driven SPA is the most natural way to build that. The blog needed crawlable, linkable, shareable text, and the fastest path to that is plain HTML that exists before any JavaScript runs.
A full Next.js migration would have meant porting GSAP timelines and Three.js setup into a framework that renders on the server first, dealing with hydration for code that only makes sense in the browser, and reworking the build and deploy pipeline — all to fix a problem I could solve by generating one directory of static files. The blog is now markdown compiled to HTML at build time, served from the same domain behind the same Cloudflare zone. The SPA stays a SPA. The content layer is fully server-free HTML. Both get what they're good at.
How Google Actually Sees a Client-Side Rendered React App
Googlebot does execute JavaScript. That fact gets repeated so often it's treated as a full stop — "don't worry, Google renders JS." But rendering happens in a second wave, separate from initial discovery and crawling, and it's queued and resource-limited. That delay is real: content that only exists after hydration can be missing from the first index of a page, and it may be indexed later, sometimes with different text if something in the pipeline failed.
Other crawlers don't render at all. Social scrapers, some SEO tool crawlers, link preview bots, and a share of AI crawlers fetch raw HTML and stop. If your headings, body copy, and meta tags are produced by React on the client, those fetchers see an empty shell. For a marketing site or blog, that's the majority of the audience you care about, and none of it is recoverable after the fact — the raw HTML is what gets shared and archived.
The practical rule I follow: anything meant to be read is delivered as HTML; anything meant to be interacted with is hydrated on top.
Where Next.js Genuinely Wins for SEO
Next.js is not hype when it comes to content-heavy sites. Static generation and server rendering give you real advantages:
- HTML on first byte. With static generation, the page is pre-rendered at build time. The crawler, the reader, and the social scraper all get the same complete document immediately.
- No JavaScript required to read. Body copy, headings, internal links, and structured data are in the initial response. JavaScript becomes progressive enhancement instead of a prerequisite.
- Routing that matches URLs. File-based routing, per-page metadata, canonical tags, sitemaps, and RSS are solved problems rather than things you assemble yourself.
- Dynamic content stays indexable. Incremental static regeneration lets product pages, docs, or blog posts update without a full rebuild while staying pre-rendered.
- Consistent rendering everywhere. The version a bot sees and the version a user sees are the same document, so you never reconcile two divergent outputs.
If I were starting a documentation site, a publication, or an SEO-driven marketing property from scratch today, I'd reach for Next.js without much hesitation. The cost of pre-rendering is low, and the cost of missing content is high.
What a React SPA Still Does Better
The counter-case is stronger than framework-war threads usually admit:
- No server, no hydration boundary. A static SPA is one bundle, one deploy, and no server-side bugs. SSR introduces hydration mismatches, streaming edge cases, and a class of errors that only appear under load.
- Animation-heavy interfaces. GSAP timelines, Three.js scenes, and scroll-linked transitions are far easier to own in a client-only app than in one that must render safely on the server first.
- Perceived smoothness. Client-side navigation swaps content without a full document load. For a portfolio or app-like site, that transition quality is part of the product.
- Operational simplicity. Static files behind a CDN and a reverse proxy are trivial to host, cache, and reason about. I serve mine as static files through Caddy behind Cloudflare.
The mistake isn't choosing an SPA. The mistake is choosing an SPA for a site whose primary job is being read by search engines.
Crawl Budget, JavaScript Cost, and Core Web Vitals
Crawl budget rarely matters for a site with hundreds of pages — it's a real constraint at tens of thousands. But JavaScript rendering still taxes every crawl: the crawler must fetch, parse, and execute your bundles before it can see links, and heavy pages consume resources that could have been spent elsewhere. Pre-rendered HTML removes that step entirely.
Core Web Vitals are where the tradeoff gets nuanced. A client-rendered page often scores fine on Largest Contentful Paint if the HTML shell and hero arrive quickly, but Total Blocking Time and Interaction to Next Paint suffer when hydration runs long, scroll handlers aren't throttled, or animation libraries take the main thread. A statically generated page starts with an enormous advantage: there's no hydration work at all for the reader who only reads. My SPA carries GSAP and Three.js, which is a cost I accept for the homepage — but the blog pages ship as plain HTML and pay none of it.
JavaScript cost is also a user cost. Every kilobyte of framework you force on a reader is a kilobyte that delays their first sentence. For a blog, that's hard to justify.
| Concern | React SPA (CSR) | Next.js / SSG |
|---|---|---|
| Content in raw HTML | No, after JS runs | Yes, at build or request time |
| Renders for non-JS crawlers | Shell only | Full document |
| Animation-heavy UX | Excellent | Good, with hydration care |
| Hosting complexity | Very low | Low to moderate |
| Core Web Vitals for content pages | Work required | Strong by default |
My Decision: Keep the SPA, Generate the Blog Statically
I didn't pick a winner between the two — I split the problem. The homepage and interactive work stay in React as a Vite SPA. Every piece of content that needs to be found lives in the static blog, compiled from markdown to HTML at build time. The two share a domain, so internal links pass authority between them, and readers move from an article into the interactive portfolio without a context switch.
If you're facing the same fork: build the SPA when the interface is the product, and reach for pre-rendering when the content is the product. If your site needs both, you don't have to choose — you can ship a static content layer next to your SPA and get the SEO benefits of Next.js without abandoning the app you already have. You can see how that plays out on my portfolio, and I've written separately about how I structured the AI website generator architecture that produces multi-page static packages.
FAQ
Does Google index client-rendered React apps?
Yes, Googlebot executes JavaScript and can index client-rendered content, but it happens in a separate, queued rendering pass. That can delay indexing, and non-rendering crawlers never see your content at all. Pre-rendered HTML removes the timing risk entirely.
Is Next.js required for good SEO?
No. Next.js makes pre-rendering easy, but any approach that puts complete HTML in the initial response — static site generation, a markdown-built blog, or server rendering — satisfies search engines. Framework choice matters far less than whether content exists in the raw document.
Will adding a static blog to my SPA cause duplicate content problems?
Not if each URL serves exactly one format. My blog lives at /blog/... paths that exist only as static HTML, and the SPA routes exist only as the app. Keep canonical tags consistent, submit one sitemap, and don't render the same article at two URLs.
How long does it take for a JavaScript-rendered page to get indexed compared to static HTML?
There's no fixed number, and I won't invent one — rendering queues vary. What's consistent is that static HTML is eligible immediately on first crawl, while client-rendered content waits for a render pass that can be deferred under load.
If you're deciding between a rewrite and a smarter content layer for your own site, tell me what you're building — get in touch and I'll tell you honestly whether Next.js is worth it for your case.