Why a client-only React app is the fastest thing to build and the most expensive thing to fix, how Next.js's App Router separates what must be findable from what only needs to be usable, and the code at the boundary between the two.
by Nadia Maar ·
A startup's first technical decision is almost never about the product: it is about how the interface gets built, and it usually gets made by whoever is in the biggest hurry to see something on screen. A React app rendered entirely on the client — no server involved in drawing the page — is the fastest thing to get moving: you write a component, it appears, you iterate. It is also the choice that makes the pricing page invisible to Google until somebody notices, usually when the first investor asks “why don't you show up when someone searches your own product name”.
The problem is not technical in the narrow sense: it is that the fix arrives late, and arriving late is what makes it expensive in a way an earlier decision would not have been. It is not a matter of adding a plugin: pages that need to be found and screens that only need to be used require two different ways of being built, and mixing them into one architecture picked for week-one speed costs a rewrite by month eight.
This piece is the decision that avoids the rewrite: how Next.js's App Router keeps, in the same project, both the pages a search engine must be able to read and the interactive dashboard it will never need to care about, the real code at that boundary, and the bill for finding out late.
A client-only app sends the browser an almost-empty page and a JavaScript file that, once run, builds everything else. For a person with a good connection the difference is barely noticeable. For a crawler that has to decide in a fraction of a second whether a page deserves indexing, that empty page is often all it sees — and “maybe” is not the answer that brings organic traffic.
It does not arrive at launch: it arrives when marketing asks why the pricing page does not show up for the product's own name, or when an investor notices that the blog — written, published, good — brings no traffic. By then the product code and the marketing code are already tangled, and untangling them is the rewrite, not a fix.
The mistake is not choosing React: it is treating every page the same way. Marketing pages — landing, pricing, blog — need to already exist, written, by the time whoever reads them arrives, human or crawler. The dashboard behind the login needs exactly the opposite: immediate interactivity, and a crawler will never care because it will never get in. Next.js's App Router lets you choose per page, in the same project, without two separate codebases to maintain.
| Client-only | Server-only | App Router · per page | |
|---|---|---|---|
| Pricing page, indexed by Google | Risky: depends on how long the crawler waits | Yes, by construction | Yes — Server Component, pre-rendered |
| Dashboard with an interactive editor | Yes, by construction | Expensive: every interaction needs a round trip to the server | Yes — Client Component, hydrated only where needed |
| Weight downloaded by a landing-page visitor | The whole app bundle, dashboard included | Minimal, but little interactivity anywhere | Only that page's markup plus the components it actually uses |
| Two codebases to maintain | No, but one approach compromises the other case | No, the same trade-off in reverse | No — one project, the choice is per file |
The row that matters is the last one in the first column: you do not have to choose between development speed and search-engine visibility. The boundary is drawn per page, with one line at the top of a file — not with a parallel architecture you maintain forever.
A pricing page is a Server Component by default: no special directive, data arrives through a normal `fetch` executed on the server, and what the browser receives is already complete HTML — a crawler does not have to wait for, or run, anything.
// No "use client" directive: this component runs ON THE SERVER,
// once per build (or per request, with revalidate set to zero).
export default async function PricingPage() {
const plans = await fetch("https://api.example.com/plans", {
next: { revalidate: 3600 }, // regenerated at most once an hour
}).then(r => r.json())
return (
<main>
<h1>Pricing</h1>
{plans.map((p: { name: string; price: number }) => (
<div key={p.name}>
<h2>{p.name}</h2>
<p>{p.price} € / month</p>
{/* Interactive island INSIDE a static page: only this piece
hydrates JavaScript in the browser, the rest stays HTML. */}
<SubscribeButton plan={p.name} />
</div>
))}
</main>
)
}The crawler sees title, prices and structure without running a single line of JavaScript, because there is none inside this component — it is already HTML by the time it leaves the server.
"use client" // The boundary: from here down, JavaScript in the browser.
import { useState } from "react"
export default function SubscribeButton({ plan }: { plan: string }) {
const [loading, setLoading] = useState(false)
async function subscribe() {
setLoading(true)
await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ plan }) })
// ...redirect to checkout
}
return (
<button onClick={subscribe} disabled={loading}>
{loading ? "One moment…" : "Choose this plan"}
</button>
)
}"use client" is the one line separating the two strategies. Everything above this boundary in the component tree stays static HTML; everything below it becomes JavaScript hydrated in the browser — a button, not the whole page.
Rewriting the rendering architecture eight months after launch is never “adding SSR”: it is a cascade, because every later choice assumes the first one. Routing, how data gets fetched, the boundary between public and authenticated pages were all written assuming everything ran in the browser.
€ 38,000 – 62,000 — typical cost of a rendering rewrite on a product with 40-60 pages, six months after launch
The figure includes time lost re-testing features that already worked — not just new code.
If you are about to start a new product, bring the list of pages you're imagining: which need to be found, which only need to be used. In half an hour we draw the boundary before it becomes a coding habit that is hard to undo.
If you already have a client-only app that works and nobody is asking you for organic traffic yet, I will tell you: it does not need rewriting today. The right moment is before the first marketing page gets written on top of the wrong architecture, not after sixty pages already have been.
Technical SEO and AI search optimization
Because the browser receives an almost-empty page and a JavaScript file that builds the content AFTER the first paint. A crawler that does not wait long enough — or that scores the page at a disadvantage regardless — sees little or nothing, and the meta tags for social previews, which must exist before anything runs, stay generic.
They are components that run on the server and produce already-complete HTML, with no JavaScript shipped to the browser for that part of the page. They are used for anything that needs to be found — landing pages, pricing, blog — while the "use client" directive marks the boundary to interactive components (forms, editors, dashboards) that need to run in the browser.
Yes, and that is exactly the App Router's advantage: the choice is made per file, not per project. A page can be almost entirely static with a single interactive island — a button, a form — hydrating JavaScript, while the rest stays plain HTML.
On a product with 40-60 pages, typically €38,000-62,000, six months after launch — and it is not just new code: it includes time re-testing features that already worked, and organic traffic lost during the weeks pages stayed poorly indexable, which does not come back by pushing the fix forward in time.