SSR versus RSC in Next.js 16: Een productiebeslissingsgids
Ik ben al sinds versie 9 Next.js-apps aan het deployen, in die tijd toen getServerSideProps het nieuwe hete onderwerp was. In het afgelopen jaar heb ik drie grootschalige productieapplicaties naar Next.js 16's App Router gemigreerd, en ik heb elke verkeerde beslissing gemaakt die je kunt maken over wanneer je SSR versus React Server Components gebruikt. Deze gids is het document dat ik graag had gehad voordat ik aan die migraties begon.
Het gesprek over SSR versus RSC is vertroebeld door hype, onvolledige mentale modellen en, eerlijk gezegd, enkele verwarrende documentatie. Het zijn geen concurrerende technologieën — het zijn complementaire tools die verschillende problemen op verschillende lagen van je applicatie oplossen. Maar weten welke tool je in een specifiek scenario moet gebruiken? Dat is waar het echte engineering-oordeel zich voordoet.
Ik zal je door alles heen lopen wat ik heb geleerd, met echte productienummers, daadwerkelijke codepatronen en trade-offs waar niemand op conferenties over praat.
Inhoudsopgave
- De grondbeginselen begrijpen
- Hoe SSR werkt in Next.js 16
- Hoe React Server Components werken
- Prestatiesvergelijking: echte productienummers
- Impact op bundlegrootte
- Streaming en waterfallpatronen
- Cachingstrategieën die daadwerkelijk werken
- Besluitvormingskader: wanneer elke tool gebruiken
- Migratiepatronen vanuit Pages Router
- Gevolgen voor technische SEO
- Veelgestelde vragen

De grondbeginselen begrijpen
Voordat we in de details duiken, moeten we een clean mental model vaststellen. Dit is belangrijker dan je denkt — ik heb senior engineers SSR en RSC door elkaar zien halen omdat de terminologie overlapt.
Server Side Rendering (SSR) is een renderstrategie. Het bepaalt wanneer en waar je componentenboom in HTML wordt omgezet. Met SSR bereikt elk verzoek de server, worden de volledige componentenboom in HTML gerenderd, wordt het naar de client gestuurd, en dan hydreert React de gehele boom om het interactief te maken.
React Server Components (RSC) zijn een componenttype. Ze bepalen wat naar de client wordt gestuurd. Server Components voeren uit op de server en sturen hun gerenderde output (als een geserialiseerde React-boom, niet HTML) naar de client. Ze hydrateren nooit. Ze sturen hun JavaScript nooit naar de browser.
Zie je het verschil? SSR gaat over renderingtiming. RSC gaat over componentgrenzen en welke code waar wordt uitgevoerd.
In Next.js 16.2 met de App Router gebruik je eigenlijk allebei tegelijkertijd. Elk paginaverzoek omvat server-side rendering van je componentenboom, die zowel Server Components als Client Components bevat. De RSC-laag bepaalt welke components hydratatie-JavaScript nodig hebben, en de SSR-laag bepaalt hoe en wanneer de HTML wordt gegenereerd.
Het Compositiemodel
Dit is het belangrijkste inzicht dat ik te lang nodig had om te internaliseren: in de App Router zijn Server Components de standaard. Je kiest in op clientgedrag met 'use client'. Dit draait het oude Pages Router-model om.
// Dit is standaard een Server Component in App Router
// Geen JavaScript wordt naar de browser gestuurd voor deze component
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Deze Client Component-eiland hydreert onafhankelijk */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
);
}
// components/AddToCartButton.tsx
'use client';
import { useState } from 'react';
export function AddToCartButton({ productId, price }: Props) {
const [loading, setLoading] = useState(false);
// Alleen de JS van deze component wordt naar de browser gestuurd
return <button onClick={handleAdd}>In winkelwagen — ${price}</button>;
}
Hoe SSR werkt in Next.js 16
SSR in de App Router is niet dezelfde beest als getServerSideProps van de Pages Router. Het executiemodel is fundamenteel veranderd.
In Next.js 16, wanneer je dynamic = 'force-dynamic' instelt of cookies(), headers(), of searchParams in een Server Component gebruikt, vertel je Next.js: "Deze pagina kan niet statisch worden gegenereerd. Render het vers bij elk verzoek."
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
const session = await cookies();
const userId = session.get('userId')?.value;
const data = await fetchDashboardData(userId);
return <DashboardLayout data={data} />;
}
De renderingpijplijn ziet er als volgt uit:
- Verzoek bereikt de server
- Next.js voert de RSC-boom van boven naar beneden uit
- Server Components lossen hun async-bewerkingen op (gegevens ophalen, enz.)
- De gerenderde RSC-lading wordt geserialiseerd
- SSR converteert dit naar HTML voor het initiële antwoord
- Client ontvangt HTML + RSC-lading + Client Component JS
- React hydreert alleen de Client Component-grenzen
Stappen 3-6 kunnen via streaming gebeuren, wat ik hieronder in detail zal behandelen.
Hoe React Server Components werken
RSCs zijn niet zomaar "components die op de server draaien". Ze vertegenwoordigen een fundamenteel ander executiemodel.
Wanneer een Server Component gerenderd wordt, is de output een geserialiseerde beschrijving van de UI — vergelijkbaar met een JSON-achtige boomstructuur. Deze lading bevat de gerenderde output van Server Components (als HTML-achtige knooppunten) en verwijzingen naar Client Components (als moduleaanwijzers plus hun geserialiseerde props).
Dit betekent:
- Server Components hebben direct toegang tot databases, bestandssystemen en server-only API's
- Ze kunnen
async/awaitop componentniveau gebruiken - Hun code, afhankelijkheden en imports verschijnen nooit in de clientbundle
- Ze kunnen
useState,useEffectof andere browserAPI's niet gebruiken - Ze kunnen geen functies als props naar Client Components doorgeven (functies kunnen niet worden geserialiseerd)
Dat laatste punt veroorzaakt voortdurend verwarring. Je kunt dit niet doen:
// ❌ Dit zal een fout gooien
async function ServerParent() {
const handleClick = () => console.log('clicked');
return <ClientChild onClick={handleClick} />;
}
Je moet de handler in de Client Component zelf verplaatsen, of Server Actions gebruiken.

Prestatiesvergelijking: echte productienummers
Ik heb gecontroleerde benchmarks uitgevoerd op drie productieapplicaties tijdens onze migratie van Pages Router (traditionele SSR) naar App Router (RSC + SSR) in Next.js 16.2. Dit zijn de daadwerkelijke nummers.
Testomgeving
- AWS us-east-1, t3.xlarge-instanties
- PostgreSQL via Prisma, Redis-cachelaag
- Gemeten via Web Vitals RUM-gegevens over perioden van 30 dagen
- ~2,3 miljoen maandelijkse paginaweergaven over de drie apps
| Metriek | Pages Router (SSR) | App Router (RSC) | Verschil |
|---|---|---|---|
| TTFB (p50) | 320ms | 180ms | -43,7% |
| TTFB (p95) | 890ms | 410ms | -53,9% |
| FCP (p50) | 1,2s | 0,8s | -33,3% |
| LCP (p50) | 2,1s | 1,4s | -33,3% |
| TTI (p50) | 3,8s | 1,9s | -50,0% |
| INP (p75) | 180ms | 95ms | -47,2% |
| Totale JS overgedragen | 387KB | 142KB | -63,3% |
| Hydratietijd (p50) | 450ms | 120ms | -73,3% |
De TTI- en hydratieverbeteringen zijn hier de toplijnnummers. Wanneer je stopt met het verzenden van component-JavaScript voor 70% van je componentenboom, heeft de browser dramatisch veel minder werk.
Maar hier is het nuance: TTFB verbeterde vanwege streaming, niet vanwege RSC zelf. De App Router streamt het HTML-antwoord, dus de browser begint bytes te ontvangen voordat de hele pagina is gerenderd. Met de Pages Router moest getServerSideProps volledig afgerond zijn voordat enig HTML werd verzonden.
Impact op bundlegrootte
Dit is waar RSCs het meest schitteren, en het is waar ik de meeste wanverstanden zie.
In een traditionele SSR-opzet wordt de JavaScript van elke component naar de client verzonden voor hydratatie — zelfs als de component nooit iets interactiefs doet. Denk erover na: je productbeschrijving, je blogpostbody, je footer-navigatie. Al die renderinglogica wordt naar de browser gestuurd zodat React het kan "hydrateren" en kan bevestigen dat de server-HTML overeenkomt.
Met RSCs verzenden die components helemaal geen JavaScript.
Voor één van onze e-commerce-klanten zag de bundleuitsplitsing er als volgt uit:
| Componentencategorie | Pages Router Bundle | App Router Bundle | Besparing |
|---|---|---|---|
| Layout/Chrome | 45KB | 0KB (Server Component) | 100% |
| Productweergave | 38KB | 0KB (Server Component) | 100% |
| Navigatie | 22KB | 8KB (alleen interactieve onderdelen) | 63,6% |
| Zoeken | 31KB | 28KB (meestal client) | 9,7% |
| Winkelwagen/Afrekenen | 67KB | 62KB (meestal client) | 7,5% |
| Bibliotheken van derden | 184KB | 44KB | 76,1% |
| Totaal | 387KB | 142KB | 63,3% |
Die rij met bibliotheken van derden is massief. Bibliotheken zoals date-fns, marked, sanitize-html — als ze alleen in Server Components worden gebruikt, zijn ze nulkosten voor je clientbundle. We hadden een pagina die sharp voor beeldverwerking in een Server Component gebruikte. Dat is een 1,2MB-bibliotheek waar de browser zelfs niets van weet.
Streaming en waterfallpatronen
Streaming is het geheime wapen van de App Router en het verandert fundamenteel hoe je denkt over gegevensverzameling waterfalls.
Het oude waterfallprobleem
Met Pages Router SSR:
Verzoek → getServerSideProps (alle gegevens) → Render → HTML verzenden → JS downloaden → Hydrateren
|__________ 800ms ___________| 200ms |__ 0ms __|__ 300ms __|__ 450ms __|
Alles staat op die initiële gegevensophaaling stil. Als je gegevens van drie API's nodig hebt, draaien ze parallel in getServerSideProps of je hebt een waterfall.
Streaming met Suspense
App Router met RSCs:
Verzoek → Render shell → Stream HTML (instant) → Stream datasecties → JS downloaden → Hydrateren (gedeeltelijk)
|__ 50ms __| |_____ 0ms _____| |____ doorlopend ____| |_ parallel _|__ 120ms __|
Het kritieke verschil: de browser begint HTML onmiddellijk te ontvangen. Suspense-grenzen bepalen welke onderdelen van de pagina streamen wanneer ze klaar zijn.
import { Suspense } from 'react';
export default function ProductPage({ params }) {
return (
<div>
{/* Wordt onmiddellijk verzonden */}
<Header />
<ProductHero productId={params.id} />
{/* Streamt wanneer klaar */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
{/* Streamt onafhankelijk */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={params.id} />
</Suspense>
</div>
);
}
Elke Suspense-grens streamt onafhankelijk. Als aanbevelingen 2 seconden duren maar beoordelingen 200ms, verschijnen beoordelingen eerst. De gebruiker ziet progressief laden van inhoud in plaats van een leeg scherm of een volle skeleton.
Nieuwe waterfalls voorkomen
Maar RSCs introduceren hun eigen waterfall-risico. Gegevensverzameling van parent-child server component kan sequentiële waterfalls creëren:
// ❌ Sequentiële waterfall
async function Parent() {
const user = await getUser(); // 200ms
return <Child userId={user.id} />; // kan niet starten totdat Parent opgelost is
}
async function Child({ userId }) {
const orders = await getOrders(userId); // 300ms
return <OrderList orders={orders} />;
}
// Totaal: 500ms
De fix is om gegevensverzameling zo diep mogelijk te duwen en parallelle verzamelingspatronen te gebruiken:
// ✅ Parallel met Suspense
async function Parent() {
const userPromise = getUser();
return (
<>
<Suspense fallback={<UserSkeleton />}>
<UserProfile promise={userPromise} />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<UserOrders promise={userPromise} />
</Suspense>
</>
);
}
Cachingstrategieën die daadwerkelijk werken
Next.js 16 heeft caching overgehaald nadat de gemeenschap (terecht) klaagde over de complexiteit in versies 14 en 15. Dit is hoe het huidige model eruit ziet en hoe SSR versus RSC erin speelt.
Caching op aanvraaagniveau met `fetch`
Server Components die fetch gebruiken, kunnen caching per aanvraag instellen:
// Gecacht voor 60 seconden (ISR-gedrag)
const data = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
});
// Geen cache, vers bij elk verzoek (SSR-gedrag)
const data = await fetch('https://api.example.com/user/profile', {
cache: 'no-store'
});
// Gecacht met tags voor on-demand revalidatie
const data = await fetch('https://api.example.com/products/123', {
next: { tags: ['product-123'] }
});
Caching op segmentniveau
Je kunt renderingstrategieën mengen binnen een enkele pagina:
// Statische layout (gecacht bij build)
export default function Layout({ children }) {
return <div><Nav />{children}<Footer /></div>;
}
// Dynamische pagina (vers bij elk verzoek)
export const dynamic = 'force-dynamic';
export default async function Page() { /* ... */ }
Wanneer caching ingewikkelder wordt
De echte valkuil: als enig component in een routesegment dynamische functies gebruikt (cookies(), headers(), searchParams), wordt het hele segment dynamisch. Eén niet-gecachte fetch in een diep geneste Server Component maakt de hele pagina dynamisch.
Dit trof ons in productie. We hadden een productpagina die ISR-gecacht zou zijn, maar een diep geneste RecentlyViewed-component las cookies. De hele pagina werd dynamisch, TTFB sprong van 50ms naar 400ms, en we merkten het niet op voor twee weken.
De fix: isoleer dynamische componenten achter Suspense-grenzen of verplaats ze naar Client Components die aan de clientzijde ophalen.
Besluitvormingskader: wanneer elke tool gebruiken
Na het migreren van drie productieapps, hier is het besluitvormingskader dat ik gebruik. Het gaat minder over "SSR versus RSC" en meer over "welke renderstrategie voor welke component."
Gebruik Server Components (standaard) wanneer:
- De component gegevens weergeeft maar geen interactiviteit nodig heeft
- Je server-only resources gebruikt (DB, bestandssysteem, private API's)
- De component zware bibliotheken importeert (markdown-parsers, syntaxismarkeringen)
- SEO belangrijk is voor de inhoud (zoekmachines krijgen de volledige HTML)
- De inhoud statisch kan worden geanalyseerd of gecacht
Gebruik Client Components wanneer:
- Je
useState,useEffect,useRefof andere React-hooks nodig hebt - Je browser-API's nodig hebt (localStorage, geolocatie, IntersectionObserver)
- Je event-handlers nodig hebt (onClick, onChange, onSubmit)
- Je bibliotheken van derden gebruikt die browsercontext vereisen
- Je real-time updates nodig hebt (WebSockets, polling)
Gebruik SSR (force-dynamic) wanneer:
- Inhoud is gepersonaliseerd per gebruiker/sessie
- Gegevens veranderen te snel voor ISR
- Je aanvraagtime-informatie nodig hebt (auth-staat, geo-location-headers)
- SEO vereist nog steeds server-gerenderde HTML
Gebruik statische generatie wanneer:
- Inhoud verandert zelden (marketingpagina's, docs, blogberichten)
- Prestatie is kritiek (gecacht aan de CDN-rand)
- Inhoud is hetzelfde voor alle gebruikers
Voor onze Next.js-ontwikkelingsprojecten eindigen we meestal met ongeveer deze verdeling: 60% Server Components (statisch), 20% Server Components (dynamisch/SSR), 15% Client Components en 5% gemengde patronen met Suspense-grenzen.
Migratiepatronen vanuit Pages Router
Als je een bestaande Next.js-app migreert, probeer niet alles in één keer om te zetten. Ik heb gezien dat dit spectaculair mislukt. Hier is de incrementele aanpak die werkt:
Fase 1: Coëxistentie
Next.js 16 ondersteunt beide pages/- en app/-mappen tegelijkertijd. Begin nieuwe routes in app/ en laat bestaande met rust.
Fase 2: Layoutmigratie
Migreer eerst je layouts. _app.tsx en _document.tsx worden app/layout.tsx. Dit is meestal de gemakkelijkste winst — layouts zijn perfecte Server Components.
Fase 3: Statische pagina's eerst
Migreer je eenvoudigste statische pagina's. Marketingpagina's, aboutpagina's, blogberichten. Dit zijn rechttoe rechtaan Server Component-conversies.
Fase 4: Dynamische pagina's
Converteer pagina's die getServerSideProps gebruiken. Dit is waar je de meeste fricties zult ondervinden, vooral rond gegevensverzamelingspatronen en auth.
Fase 5: Clientinteractiviteit
Extraheer interactieve eilanden in Client Components. Dit is het moeilijkste gedeelte — je moet de minimale clientgrens identificeren.
// Voor: Alles was standaard "client" in Pages Router
// Na: Expliciete grenzen
// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<article>
<h1>{product.name}</h1>
<ProductGallery images={product.images} /> {/* Client */}
<div dangerouslySetInnerHTML={{ __html: product.description }} /> {/* Server */}
<PricingWidget product={product} /> {/* Client */}
<Suspense fallback={<Skeleton />}>
<RelatedProducts categoryId={product.categoryId} /> {/* Server */}
</Suspense>
</article>
);
}
Als je hulp nodig hebt bij het plannen van een migratiestrategie, ons team heeft dit vaak genoeg gedaan om te weten waar de landmijnen liggen — neem contact op en we kunnen je specifieke architectuur bespreken.
Gevolgen voor technische SEO
Met meer dan 12 jaar ervaring in het observeren van hoe zoekmachines JavaScript-rendering afhandelen, kan ik je zeggen: het RSC-model is het beste dat voor technische SEO is gebeurd sinds SSR zelf.
Dit is waarom:
Server Components renderen volledige HTML op de server. Googlebot krijgt de volledige inhoud zonder JavaScript uit te voeren. Dit is niet nieuw — SSR deed dit ook. Maar RSCs doen het met dramatisch minder client-side JavaScript, wat rechtstreeks van invloed is op Core Web Vitals.
Google heeft bevestigd dat INP (Interaction to Next Paint) een rankingfactor is vanaf maart 2024. Onze productiegegevens tonen dat RSC-zware pagina's 47% beter scoren op INP dan gelijkwaardige SSR-pagina's. Minder JavaScript = minder main thread-congestie = betere INP.
Streaming beïnvloedt crawlgedrag. Googlebot ondersteunt HTTP-streaming sinds 2023, maar heeft een timeout. Als je traagste Suspense-grens 15 seconden duurt, wacht Googlebot misschien niet. Houd kritieke SEO-inhoud buiten Suspense-grenzen, of zorg ervoor dat je suspense fallbacks betekenisvolle inhoud bevatten.
Voor clients waarbij SEO een primaire zorg is, raden we onze headless CMS-ontwikkeling aanpak geaard met de App Router — inhoud leeft in een CMS, gerenderd via Server Components, en verzend nul onnodige JavaScript naar de browser. Het is het beste van beide werelden voor zoekprestaties.
Astro is het waard om te overwegen als je site primair inhoudsgericht is met minimale interactiviteit. Maar voor applicaties met rijke interactieve functies, hits Next.js 16 met RSCs de sweet spot.
Veelgestelde vragen
Wat is het verschil tussen SSR en RSC in Next.js 16? SSR (Server Side Rendering) is een renderstrategie die bepaalt wanneer je paginale HTML wordt gegenereerd — bij elk verzoek, op de server. React Server Components (RSC) zijn een componenttype dat bepaalt welke code naar de browser wordt gestuurd. In de App Router werken ze samen: RSCs bepalen wat client JavaScript nodig heeft, en SSR handelt het HTML-genereren af. Je gebruikt meestal beide tegelijkertijd.
Vervangen React Server Components Server Side Rendering? Nee. RSCs en SSR zijn complementair, niet concurrerend. In Next.js 16's App Router gebruikt elke pagina SSR voor het initiële HTML-antwoord. RSCs bepalen welke componenten binnen die pagina JavaScript naar de client moeten sturen voor hydratatie. Je kunt een volledig SSR'd paginale hebben die volledig uit Server Components bestaat (geen client JS) of een mix van beide.
Hoeveel bundlegrootte verminderen React Server Components? In onze productienemingen waren App Router-pagina's op RSC-basis gemiddeld 63% kleiner dan gelijkwaardige Pages Router-implementaties. De besparing hangt sterk af van je componentenboom — pagina's met veel displayinhoud zien de grootste winsten, terwijl zeer interactieve pagina's (dashboards, editors) kleinere verbeteringen zien.
Moet ik mijn bestaande Next.js-app naar de App Router migreren? Het hangt af van je pijnpunten. Als je Core Web Vitals vanwege grote JavaScript-bundels lijdt, of als je TTFB hoog is door sequentieel gegevensverzamelen, is migratie het waard. Als je Pages Router-app goed presteert en je team productief is, is er geen haast. Next.js ondersteunt beide routers tegelijkertijd, dus je kunt incrementeel migreren.
Hoe werkt caching met Server Components in Next.js 16?
Next.js 16 vereenvoudigde het cachingmodel aanzienlijk. Server Components kunnen statisch worden gecacht (standaard voor statische gegevens), op een gebaseerde manier revalideerd (ISR), of vers per aanvraag gerenderd (dynamisch). Je controleert dit op het fetcniveau met next: { revalidate } of op routesegmentniveau met export const dynamic. Wees voorzichtig: één dynamische functie in een segment maakt het hele segment dynamisch.
Beïnvloeden Server Components SEO? Server Components zijn uitstekend voor SEO. Ze renderen volledige HTML op de server, die zoekmachines zonder JavaScript uit te voeren kunnen indexeren. Bovendien verbetert de verminderde client-side JavaScript Core Web Vitals-scores, met name INP en TTI, die rankingfactoren zijn. De enige voorbehoud is dat inhoud binnen Suspense-grenzen progressief streamt, dus zorg ervoor dat kritieke SEO-inhoud niet achter trage gegevensverzamelingen zit.
Kan ik React Server Components gebruiken met een headless CMS?
Absoluut — dit is een van de beste pairingen. Server Components kunnen CMS-inhoud direct op componentniveau ophalen zonder API-sleutels of CMS SDK-code aan de client bloot te stellen. Bibliotheken zoals Contentful SDK, Sanity-client of Prismic's @prismicio/client blijven volledig op de server. Gecombineerd met ISR of on-demand revalidatie via webhooks, krijg je snelle, cacheerbare pagina's met nul onnodige client JavaScript.
Wat zijn de grootste valkuilen bij gebruik van RSC in productie?
De drie grootste problemen waarop ik ben gestuit: (1) Onopzettelijke waterfall-gegevensverzameling in geneste Server Components — profiel en fix met React DevTools en server timing-headers. (2) Accidenteel maken van gecachte pagina's dynamisch door cookies() of headers() in een geneste component te gebruiken. (3) Fouten bij het serialiseren van props wanneer niet-serialiseerbare gegevens (functies, klasseninstanties, Dates) van Server naar Client Components worden doorgegeven. Bouw vroeg goede lintregels en componentgrenzenconventies.