Partial prerendering: statische shell, dynamische gaten
Combineer een vooraf gerenderde statische shell met gestreamde dynamische regio’s volgens het PPR-model.
Partial prerendering: statische shell, dynamische gaten is een gratis Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)-les op CoddyKit. Dit is les 3 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Fullstackontwikkeling met Next.js 15 (App Router + Server Actions). Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.
Wat is gedeeltelijk vooraf renderen?
Gedeeltelijk vooraf renderen (PPR) is een model voor het renderen dat is geïntroduceerd in Next.js 14+ en verder is verfijnd in Next.js 15. Hiermee kan één route tegelijk zowel statische als dynamische inhoud aanbieden.
Bij traditioneel renderen moest u één optie kiezen:
- Statisch (SSG/SSR met volledige cache) — snel, maar verouderd voor gepersonaliseerde gegevens
- Dynamisch (SSR bij elk verzoek) — actueel, maar trager bij de eerste byte
PPR doorbreekt die afweging. Tijdens het bouwen genereert Next.js vooraf een statische schil (indeling, omlijsting, inhoud boven de vouw) en maakt het gaten op plaatsen waar dynamische gegevens tijdens het verzoek binnenkomen — allemaal vanuit één routebestand.
PPR inschakelen in Next.js 15
PPR is een functie waarvoor u zich expliciet moet aanmelden. Schakel deze in next.config.ts in met de vlag experimental.ppr.
In Next.js 15 kunt u PPR ook stapsgewijs inschakelen — per route — met de configuratie-export experimental_ppr voor een routesegment, in plaats van de functie globaal in te schakelen.
Na het inschakelen analyseert het framework tijdens het bouwen uw routestructuur en scheidt het de statische schil van deelbomen die dynamische API's bevatten (cookies(), headers(), searchParams, noStore() enzovoort).
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
ppr: true, // enable globally
// OR use 'incremental' to opt in per-route
// ppr: 'incremental',
},
};
export default nextConfig;Een route stapsgewijs aanmelden voor PPR
Wanneer u ppr: 'incremental' instelt in next.config.ts, moeten afzonderlijke routesegmenten zich expliciet aanmelden door de constante experimental_ppr met de waarde true te exporteren.
Dit is ideaal om grote apps geleidelijk te migreren: alleen de pagina's die u annoteert krijgen PPR, terwijl de rest zich gedraagt zoals voorheen.
De export moet rechtstreeks in het paginabestand of indelingsbestand van het segment staan dat u gedeeltelijk vooraf wilt renderen.
// app/dashboard/page.tsx
// Opt this route into Partial Prerendering
export const experimental_ppr = true;
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/* static shell renders at build time */}
{/* dynamic holes are defined below with Suspense */}
</main>
);
}De statische schil
De statische schil is alles in uw route dat niet afhankelijk is van dynamische API's. Next.js rendert en cached dit tijdens het bouwen vooraf (klaar voor een CDN), zodat het direct wordt geleverd — voordat een databaseaanroep wordt uitgevoerd.
Typische inhoud van de statische schil omvat:
- Pagina-indeling en navigatieomlijsting
- Hero-secties met hardgecodeerde of statisch opgehaalde tekst
- Skeleton-/tijdelijke aanduidingsinterface voor de dynamische gaten
- Server Components die alleen het bestandssysteem of een statische
fetchmet volledige cache lezen
Het belangrijkste inzicht: de statische schil wordt geleverd als een vooraf gegenereerde HTML-stroom, terwijl de dynamische gaten parallel verder laden.
// app/store/page.tsx
import { Suspense } from 'react';
import ProductListSkeleton from '@/components/ProductListSkeleton';
import DynamicCart from '@/components/DynamicCart';
import StaticHero from '@/components/StaticHero'; // no dynamic APIs
export const experimental_ppr = true;
export default function StorePage() {
return (
<div>
{/* Static shell — prerendered at build time */}
<StaticHero />
<nav>Store Navigation</nav>
{/* Dynamic hole — streams in at request time */}
<Suspense fallback={<ProductListSkeleton />}>
<DynamicCart />
</Suspense>
</div>
);
}Dynamische gaten definiëren met Suspense
Een dynamisch gat is elke deelboom die is omwikkeld met een <Suspense>-grens en componenten bevat die dynamische Next.js-API's gebruiken.
Next.js gebruikt de Suspense-grens als grens tussen statisch en dynamisch. Alles binnen de grens wordt uitgesloten van de vooraf gegenereerde schil en in plaats daarvan als een afzonderlijk deel gestreamd zodra de gegevens klaar zijn.
Belangrijke regels:
- De dynamische Server Component moet de async-component zijn die de gegevens ophaalt — niet alleen een wrapper
- U moet een
fallbackopgeven voor de Suspense-grens; deze fallback wordt in de statische schil opgenomen, zodat gebruikers deze direct zien - Meerdere onafhankelijke Suspense-grenzen maken meerdere onafhankelijke gaten die parallel worden gestreamd
// components/DynamicCart.tsx
import { cookies } from 'next/headers'; // marks this subtree as dynamic
async function DynamicCart() {
const cookieStore = await cookies();
const sessionId = cookieStore.get('session_id')?.value;
const cart = sessionId
? await fetchCart(sessionId) // DB call at request time
: null;
return (
<aside>
<h2>Your Cart</h2>
{cart ? (
<ul>
{cart.items.map((item) => (
<li key={item.id}>{item.name} — ${item.price}</li>
))}
</ul>
) : (
<p>Your cart is empty.</p>
)}
</aside>
);
}
export default DynamicCart;
async function fetchCart(sessionId: string) {
// simulated DB call
return { items: [{ id: 1, name: 'Widget', price: 9.99 }] };
}Meerdere onafhankelijke dynamische gaten
Een van de grootste voordelen van PPR is dat u meerdere onafhankelijke dynamische gaten in één route kunt hebben. Elke Suspense-grens streamt de inhoud onafhankelijk, zodat een trage databaseaanroep in het ene gebied een ander gebied niet blokkeert.
Denk aan een dashboard met:
- Een begroeting voor de gebruiker (leest
cookies()) - Een widget met live statistieken (leest uit een trage analysedatabase)
- Een feed met recente activiteit (leest uit een andere tabel)
Alle drie worden parallel gestreamd nadat de statische schil is geleverd. Gebruikers krijgen zo een geleidelijk verbeterde ervaring in plaats van één lange wachttijd.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import UserGreeting from '@/components/UserGreeting';
import LiveStats from '@/components/LiveStats';
import ActivityFeed from '@/components/ActivityFeed';
import {
GreetingSkeleton,
StatsSkeleton,
FeedSkeleton,
} from '@/components/Skeletons';
export const experimental_ppr = true;
export default function DashboardPage() {
return (
<main className="dashboard-grid">
<h1>Welcome to your Dashboard</h1> {/* static */}
{/* Three independent holes — stream in parallel */}
<Suspense fallback={<GreetingSkeleton />}>
<UserGreeting />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<LiveStats />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
</main>
);
}Wat maakt een component dynamisch?
Next.js detecteert dynamisch gebruik automatisch door naar specifieke API's te zoeken. Een Server Component wordt als dynamisch beschouwd (en uitgesloten van de statische schil) wanneer deze het volgende gebruikt:
cookies()ofheaders()uitnext/headers- De prop
searchParamsop een pagina unstable_noStore()offetchmetcache: 'no-store'- Elke API die tijdens een verzoek afziet van de Data Cache
Als buiten een Suspense-grens een dynamische API wordt gebruikt, valt de hele route terug op volledig dynamisch renderen — PPR wordt voor die route uitgeschakeld. Wikkel dynamische componenten altijd in Suspense om de statische schil te behouden.
// components/UserGreeting.tsx
import { cookies, headers } from 'next/headers';
export async function UserGreeting() {
// Both of these mark this component as dynamic
const cookieStore = await cookies();
const headersList = await headers();
const userId = cookieStore.get('user_id')?.value;
const userAgent = headersList.get('user-agent') ?? 'Unknown';
const user = userId ? await fetchUser(userId) : null;
return (
<div>
<p>Hello, {user?.name ?? 'Guest'}!</p>
<small>Browsing via: {userAgent.split('/')[0]}</small>
</div>
);
}
async function fetchUser(id: string) {
// imagine a real DB query here
return { name: 'Mehmet' };
}Skeletonfallbacks maken deel uit van de statische schil
De prop fallback van elke Suspense-grens wordt opgenomen in de vooraf gegenereerde statische schil. Dit betekent dat gebruikers de skeletoninterface zonder enige vertraging ontvangen — met dezelfde latentie als bij een volledig statische pagina.
Ontwerp uw skeletons zo dat ze de afmetingen en indeling van de uiteindelijke inhoud zo goed mogelijk benaderen. Dit beperkt Cumulative Layout Shift (CLS) wanneer de dynamische inhoud binnenkomt en de skeleton vervangt.
Een goede skeletonstrategie:
- Laat de breedte en hoogte zo nauw mogelijk overeenkomen met die van de echte inhoud
- Gebruik CSS-animaties (puls/shimmer) om de laadstatus aan te geven
- Vermijd tijdelijke aanduidingen die op echte gegevens lijken — gebruikers kunnen ze als inhoud lezen
// components/Skeletons.tsx
export function StatsSkeleton() {
return (
<div className="animate-pulse space-y-2 p-4 rounded-lg bg-gray-100">
<div className="h-4 bg-gray-300 rounded w-1/3" />
<div className="h-8 bg-gray-300 rounded w-1/2" />
<div className="h-4 bg-gray-300 rounded w-2/3" />
</div>
);
}
export function FeedSkeleton() {
return (
<ul className="animate-pulse space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<li key={i} className="flex gap-3 items-center">
<div className="h-8 w-8 rounded-full bg-gray-300" />
<div className="flex-1 h-4 bg-gray-300 rounded" />
</li>
))}
</ul>
);
}
export function GreetingSkeleton() {
return <div className="animate-pulse h-6 bg-gray-300 rounded w-40" />;
}PPR versus traditioneel streaming-SSR
U kunt PPR gemakkelijk verwarren met gewoon streaming-SSR (waarbij u Suspense zonder PPR gebruikt). Dit is het belangrijkste verschil:
- Streaming-SSR (zonder PPR) — Het volledige antwoord wordt tijdens het verzoek op de server gerenderd. De statische delen worden snel gerenderd en de dynamische delen worden later gestreamd. De eerste HTML wordt echter nooit aan de CDN-rand gecached — elk verzoek bereikt uw server.
- PPR — De statische schil wordt tijdens het bouwen vooraf gegenereerd en direct vanaf de CDN-rand geleverd. Alleen de dynamische gaten activeren tijdens het verzoek serveruitvoering. U krijgt zowel statische levering met CDN-snelheid als actuele dynamische gegevens.
PPR is in feite: statische levering via CDN + streamen tijdens het verzoek, gecombineerd in één route.
searchParams lezen zonder PPR te verbreken
In Next.js 15 is searchParams op een Page-component een dynamische waarde: door deze te lezen schakelt u de hele pagina uit voor statisch renderen. Met PPR mag u searchParams niet rechtstreeks in de pagecomponent lezen (buiten een Suspense-grens).
Geef searchParams in plaats daarvan als prop door aan een component die in Suspense is gewikkeld. Zo blijft de buitenste paginaschil statisch, terwijl de binnenste component queryparameters tijdens het verzoek kan lezen.
// app/search/page.tsx
import { Suspense } from 'react';
import SearchResults from '@/components/SearchResults';
import SearchResultsSkeleton from '@/components/SearchResultsSkeleton';
export const experimental_ppr = true;
type SearchPageProps = {
searchParams: Promise<{ q?: string; page?: string }>;
};
export default function SearchPage({ searchParams }: SearchPageProps) {
// Do NOT await searchParams here — that would make the shell dynamic.
// Pass the Promise into the dynamic hole instead.
return (
<div>
<h1>Search</h1> {/* static shell */}
<Suspense fallback={<SearchResultsSkeleton />}>
{/* SearchResults awaits searchParams inside the Suspense boundary */}
<SearchResults searchParamsPromise={searchParams} />
</Suspense>
</div>
);
}PPR-gedrag controleren tijdens ontwikkeling
Tijdens next dev wordt PPR gesimuleerd, maar pagina's worden niet echt vooraf gebouwd — bij elk verzoek worden ze opnieuw weergegeven. Om het daadwerkelijke PPR-gedrag te bekijken, moet je een productiebuild uitvoeren:
next build— genereert de statische schil tijdens het bouwennext start— serveert de productie-uitvoer
Controleer na een build de uitvoertabel die in de terminal wordt afgedrukt. Routes met PPR worden gemarkeerd met het symbool ◐ (halfgevulde cirkel), wat aangeeft dat ze gedeeltelijk vooraf worden weergegeven — anders dan volledig statische routes (○) en volledig dynamische routes (λ).
Je kunt ook de map .next/server/app bekijken voor het vooraf weergegeven HTML-bestand en de afzonderlijke RSC-payload voor de dynamische plekken.
Kennistoets: regels voor PPR-grenzen
Een ontwikkelaar schakelt PPR in voor een route en wikkelt zijn dynamische component in een Suspense-grens. Welke van de volgende uitspraken beschrijft correct wat er gebeurt als de ontwikkelaar cookies() rechtstreeks in de Page-component aanroept (buiten elke Suspense-grens)?
Samenvatting: gedeeltelijke voorafgaande weergave in de praktijk
In deze les heb je Partial Prerendering (PPR) verkend — het Next.js 15-model dat statische levering met CDN-snelheid combineert met dynamische streaming tijdens verzoeken.
Belangrijkste punten:
- Schakel PPR globaal in via
experimental.ppr: trueinnext.config.ts, of per route metexport const experimental_ppr = true - De statische schil (alles buiten Suspense-grenzen) wordt tijdens het bouwen vooraf weergegeven en direct vanuit de CDN geleverd
- Elke Suspense-grens definieert een dynamische plek — de terugvalweergave ervan wordt in de schil opgenomen en de inhoud wordt tijdens het verzoek gestreamd
- Dynamische API's (
cookies(),headers(),searchParams,no-store-fetches) moeten binnen Suspense-grenzen staan om de schil te behouden - Meerdere onafhankelijke Suspense-grenzen worden parallel gestreamd, waardoor de waargenomen prestaties maximaal worden
- Bouw met
next builden zoek naar het symbool ◐ om te bevestigen dat PPR actief is op een route
PPR vormt de brug tussen de alles-of-nietskeuze voor statische of dynamische inhoud — gebruik het om snelle schillen en actuele gegevens vanuit hetzelfde routebestand te leveren.
Leer TypeScript met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 22
- Lessen
- 88
Veelgestelde vragen
Is de les “Partial prerendering: statische shell, dynamische gaten” gratis?
Ja — je kunt hier op het web alle 3 lessen van het leerpad Fullstackontwikkeling met Next.js 15 (App Router + Server Actions), waaronder “Partial prerendering: statische shell, dynamische gaten”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.
Wat leer ik in “Partial prerendering: statische shell, dynamische gaten”?
Combineer een vooraf gerenderde statische shell met gestreamde dynamische regio’s volgens het PPR-model. Je oefent met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) te beginnen?
Ervaring vooraf is niet nodig. Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.
Hoe lang duurt de les “Partial prerendering: statische shell, dynamische gaten”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)?
Ja. Elke les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Suspense-boundaries en streaming op componentniveau
- Zinvolle loading.tsx-bestanden en skeletons maken
- Partial prerendering: statische shell, dynamische gaten
- Valkuilen bij streaming: layout shift en waterfalls