When the App Router landed, "rendering strategy" stopped being a deploy-time decision
and became a per-component one. Server Components, Suspense streaming, and Partial
Prerendering give us a much finer set of dials than getStaticProps ever did — but
the choice of which to pull is the new puzzle.
Three knobs, four outcomes
The mental model I keep returning to:
- Server Component — runs on the server, ships zero JS.
- Client Component — needs interactivity, costs hydration.
- Suspense boundary — turns a slow read into streaming HTML.
Combine them and you can express "static shell + streamed personalisation" in a few lines:
export async function Page() {
return (
<Layout>
<Hero />
<Suspense fallback={<Skeleton />}>
<PersonalFeed />
</Suspense>
</Layout>
)
}Where it gets tricky
The cost of an RSC isn't zero. Server roundtrips add latency, and big payloads can hurt time-to-interactive even though they ship no JS.
If your RSC payload is bigger than your hydrated bundle would have been, you've made things worse.
What I'd do differently
Start with a fully static shell. Add Suspense boundaries around fetch calls that
depend on the user. Reach for 'use client' only when you genuinely need
state or effects.