Partial Prerendering i Next.js 15: Komplet PPR-guide til 2026
Lær hvordan Partial Prerendering kombinerer statisk HTML-shell med streamede Suspense-grænser i Next.js 15, og se hvordan du sætter det op rute-for-rute med praktiske kodeeksempler.
Partial Prerendering (PPR) er en Next.js-renderingstrategi, der kombinerer en statisk forudgenereret HTML-shell med dynamisk streamede komponenter i samme rute. Brugeren modtager det statiske indhold øjeblikkeligt fra en CDN-edge, mens dynamiske dele (såsom personaliserede widgets, cart-tilstand eller A/B-eksperimenter) streames ind via React Suspense. I praksis betyder det, at du kan have ét endpoint, der både er hurtigt som en statisk side og personligt som en SSR-rute, uden at vælge mellem force-static og force-dynamic. So, lad os dykke ned i, hvordan PPR fungerer i Next.js 15.3+, hvornår du skal aktivere det, hvordan du strukturerer dine komponenter omkring Suspense-grænser, og hvilke faldgruber jeg er stødt på i produktion.
Partial Prerendering serverer en statisk HTML-shell fra CDN'en på første byte, og streamer derefter dynamiske Suspense-grænser ind i samme HTTP-response.
PPR aktiveres med experimental.ppr: 'incremental' i next.config.ts og opt-in pr. rute via export const experimental_ppr = true.
Hver dynamisk del skal pakkes i <Suspense> med en fallback. Alt uden Suspense bliver inkluderet i den statiske shell.
PPR erstatter ikke ISR eller use cache, men kombineres med begge: shell'en præ-rendres, mens dynamiske huller bruger frisk data.
PPR er stadig eksperimentelt i Next.js 15.3 og kræver Canary-builds for fuld funktionalitet. Vercel anbefaler "incremental" mode til produktion.
PPR reducerer TTFB markant for personaliserede sider. I mine målinger 40-70% sammenlignet med ren SSR for typiske e-commerce-routes.
Hvad er Partial Prerendering i Next.js?
Partial Prerendering er en hybrid renderingmodel, der blev introduceret som eksperimentel i Next.js 14 og modnet betydeligt i Next.js 15.3, hvor den nu understøtter både Node.js- og Edge-runtimes uden separat opt-in. Den løser et problem, som har plaget App Router-udviklere siden 2023: tvunget valg mellem en hurtig statisk side eller en personaliseret dynamisk side. Med PPR kan en enkelt rute have begge dele i samme response.
Forestil dig en produktsides typiske struktur. Header, navigation, produkttitel, billeder, beskrivelse og anmeldelser er stort set ens for alle brugere. Til gengæld er "Anbefalede til dig", varekurv-indikator og personaliseret pris dynamisk. Før PPR var du tvunget til enten at gøre hele siden dynamisk (langsom TTFB) eller bruge client-side fetching (CLS, dårlig SEO, dårlig Core Web Vitals). PPR lader dig præ-rendre det fælles og kun streame det personlige.
Den tekniske kerne er, at Next.js ved build-tid renderer en statisk HTML-shell, der inkluderer Suspense-fallbacks som pladsholdere. Når en request rammer serveren, sendes shell'en øjeblikkeligt fra CDN'en (ofte under 50ms TTFB), og serveren begynder samtidig at rendere de dynamiske grænser. Resultaterne streames ind i samme response via Reacts Suspense streaming-protokol, og browseren udskifter pladsholderne med ægte indhold, efterhånden som de ankommer.
Hvordan fungerer PPR teknisk?
For at forstå PPR i dybden er det nyttigt at se på, hvad der sker på tre tidspunkter: ved build, ved request og i browseren. Ved build-tid analyserer Next.js compileren hver rute med experimental_ppr = true og identificerer Suspense-grænser, der indeholder dynamiske API-kald (cookies(), headers(), searchParams, ikke-cached fetch). Alt uden for disse grænser eksekveres prerender-faserne, og resultatet gemmes som en statisk HTML-fil i .next/server/app/.
Ved request-tid serveres den statiske shell direkte fra Vercels Edge Network (eller dit eget CDN) uden serverkald. Samtidig spawner Next.js dynamiske renders for hver Suspense-grænse på serveren. Disse renders bruger React Server Components til at producere et stream af "RSC payload"-chunks, som indlejres i HTML-responsens body via en speciel <script>-protokol. Browseren modtager shell + fallbacks med det samme og opdaterer DOM'en, efterhånden som chunks ankommer.
Den vigtige indsigt er, at PPR ikke er to separate requests. Det er én HTTP-response, der starter med statisk indhold og fortsætter med streamed dynamisk indhold. Det betyder ingen waterfall, ingen ekstra TLS-handshakes, og søgemaskiner som Googlebot ser det fulde indhold, når de venter på responsen.
Sådan aktiverer du PPR i Next.js 15
I Next.js 15.3 og nyere aktiveres PPR i to trin. Først skal du opdatere next.config.ts med eksperimentel-flaget. Bemærk at "incremental" mode er den eneste produktionsanbefalede tilstand. Den lader dig opt-in pr. rute i stedet for at omskifte hele appen.
Dernæst eksporterer du PPR-flaget fra de specifikke page.tsx- eller layout.tsx-filer, der skal bruge det:
// app/products/[slug]/page.tsx
import { Suspense } from 'react'
import { ProductDetails } from './product-details'
import { PersonalizedRecommendations } from './recommendations'
import { RecommendationsSkeleton } from './skeletons'
export const experimental_ppr = true
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return (
<main>
{/* Statisk del - inkluderet i prerenderet shell */}
<ProductDetails slug={slug} />
{/* Dynamisk del - streames per request */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations slug={slug} />
</Suspense>
</main>
)
}
Når du bygger appen med next build, vil du se nye markeringer i build-outputtet: ◐ for ruter med PPR aktiveret, hvor den statiske procentdel også vises. Hvis du ser advarsler om "dynamic IO not wrapped in Suspense", betyder det, at en af dine komponenter kalder dynamiske API'er uden for en Suspense-grænse. Det er en hård fejl i PPR-mode og skal fixes før deploy. (Jeg blev brændt af præcis det her, da jeg shippede en hotfix sidste forår, så ret det med det samme.)
Suspense-grænser i praksis
Den vigtigste mental model med PPR er, at Suspense-grænser definerer den dynamiske/statiske skillelinje. Alt uden for et <Suspense>-træ skal være statisk-renderbart ved build-tid. Det betyder ingen brug af cookies(), headers(), draftMode(), eller fetch uden cache uden for Suspense. Hvis du har brug for disse, skal komponenten flyttes inden i en Suspense-grænse.
I praksis arbejder jeg med tre mønstre:
Outer-shell pattern: layout og navigation er fuldt statisk; sidens hovedindhold er statisk; kun små personlige widgets er i Suspense.
Inner-content pattern: layout, nav og side-shell er statisk, men hovedindholdet er Suspense-wrapped (typisk for dashboards).
Mixed pattern: flere Suspense-grænser på samme side, hver med sin egen fallback. Det er bedst til komplekse sider som checkout med varekurv, anbefalinger og lager-status.
// Eksempel på mixed pattern
export const experimental_ppr = true
export default function CheckoutPage() {
return (
<div className="grid grid-cols-2 gap-8">
<section>
<h1>Kassen</h1>
<StaticPromoBanner />
<Suspense fallback={<CartSkeleton />}>
<CartItems /> {/* læser cookies() */}
</Suspense>
</section>
<aside>
<Suspense fallback={<ShippingSkeleton />}>
<ShippingOptions /> {/* læser headers() for region */}
</Suspense>
<Suspense fallback={<TotalSkeleton />}>
<OrderTotal /> {/* friske priser fra API */}
</Suspense>
</aside>
</div>
)
}
PPR vs SSR vs ISR vs SSG: hvornår skal du bruge hvad?
Mange udviklere er forvirrede over, hvordan PPR passer ind i Next.js' eksisterende renderingstrategier. Her er en sammenligning af de fire vigtigste modeller i 2026:
Egenskab
SSG
ISR
SSR
PPR
TTFB
Meget hurtig (CDN)
Meget hurtig (CDN)
Langsom (server)
Meget hurtig (CDN)
Personalisering
Nej
Nej
Ja
Ja (i Suspense)
Friskt data
Ved build
Periodisk revalidate
Hver request
Statisk + dynamisk pr. del
SEO
Perfekt
Perfekt
God
Perfekt for shell
Kompleksitet
Lav
Lav
Medium
Medium-høj
Bedst til
Blog, dokumentation
Marketing, kataloger
Dashboards, admin
E-commerce, hybrid
Min tommelfingerregel: brug SSG til indhold uden personalisering (dokumentation, marketingsider), ISR når du har frisk-data-behov men ingen brugerspecifik logik (produktkataloger, blogposts), SSR når hele siden skal være dynamisk (admin-paneler), og PPR når du har en blanding af statisk og dynamisk indhold på samme side. PPR fungerer fint sammen med use cache-direktivet til moderne caching, fordi du kan cache de statiske delkomponenter aggressivt og lade Suspense-grænserne bruge frisk data.
Lad os bygge et realistisk eksempel: en produktside, der kombinerer cached produktdata, brugerens varekurv-tilstand og personaliserede anbefalinger. Vi opdeler i fire komponenter, hver med klart definerede statiske/dynamiske egenskaber:
Den statiske ProductHero bruger use cache til at læse produktdata fra databasen ved build og ved revalidering. Den dynamiske CartIndicator læser cookies for at vise, om varen allerede er i kurven:
// app/products/[slug]/cart-indicator.tsx
import { cookies } from 'next/headers'
import { getCartItems } from '@/lib/cart'
export async function CartIndicator({ productSlug }: { productSlug: string }) {
const cookieStore = await cookies()
const sessionId = cookieStore.get('session_id')?.value
if (!sessionId) return null
const items = await getCartItems(sessionId)
const inCart = items.some((item) => item.slug === productSlug)
return inCart ? (
<p className="text-green-600">Allerede i din kurv</p>
) : null
}
Når en bruger besøger siden, modtager browseren shell'en med produktinfo og anmeldelser øjeblikkeligt fra CDN'en. Samtidig streamer serveren CartIndicator og Recommendations ind, så snart de er klar. Brugeren oplever en hurtig, fuldt funktionel side. Hvis du implementerer formularer på siden (fx "Tilføj til kurv"), kan du parre PPR med Server Actions til formularhåndtering og validering for at få den bedste totaloplevelse.
Almindelige fejl og fejlfinding
Efter at have rullet PPR ud i flere produktionsprojekter har jeg samlet en liste over de fejl, der oftest dukker op. De fleste handler om at glemme Suspense-grænser eller misforstå, hvilke API'er der gør en komponent dynamisk.
Dynamic API uden for Suspense
Den hyppigste fejl: en komponent kalder cookies() eller headers() uden at være pakket i Suspense. Build-fejlen ser typisk sådan ud:
Error: Route "/products/[slug]" used `cookies()` inside a component that
was statically rendered as part of Partial Prerendering. Wrap the component
in a Suspense boundary or move the data fetching to a dynamic page.
Løsning: flyt den dynamiske komponent inden for en <Suspense> med en passende fallback. Hvis komponenten ikke kan flyttes, må du opgive PPR for den specifikke rute.
Fetch uden cache-strategi
I Next.js 15.3 er fetch ikke længere cached by default. Det betyder, at fetch('https://api.example.com') uden cache: 'force-cache' eller next: { revalidate: 60 } bliver behandlet som dynamisk og skal være i Suspense. Tilføj eksplicit cache-instruktion til kald, der skal indgå i den statiske shell.
Layout læser dynamiske API'er
Hvis et layout.tsx-fil læser cookies eller headers, smitter det af på alle nested ruter. Resultatet er, at PPR effektivt deaktiveres for hele sektionen. Løsningen er at flytte den dynamiske logik ud i en client component eller en separat server component, der wrappes i Suspense inde i layoutet.
Manglende fallback-højde
Hvis din Suspense-fallback har en anden højde end det streamede indhold, oplever brugeren et layoutskift, der ødelægger CLS-scoren. Brug eksplicitte højder eller aspect-ratio-CSS på dine skeletons for at matche det forventede indhold.
Er Partial Prerendering klar til produktion?
Det korte svar: ja, hvis du bruger Vercel og er villig til at køre på Canary eller den seneste 15.3+ stable. Vercel har kørt PPR på deres egen platform siden sommeren 2024, og den er testet under høj last på e-commerce og dashboards. Det "incremental"-mode flag er specifikt designet, så du kan rulle PPR ud rute-for-rute uden at omskifte hele applikationen.
Der er dog forbehold. PPR er stadig markeret som eksperimentelt i Next.js' stable releases, hvilket betyder, at API'en teoretisk kan ændre sig. Self-hosted setups skal sikre, at deres reverse proxy (nginx, Caddy) understøtter HTTP/1.1 chunked transfer encoding korrekt. Gamle konfigurationer buffer hele responsen, hvilket ødelægger streaming. Cloudflare's standard Pages-deployments understøttede tidligere ikke streaming responses i alle planer; tjek deres officielle dokumentation for streaming-support.
Hvis du allerede har konfigureret middleware med edge runtime til rutebeskyttelse, bør du være opmærksom på, at PPR fungerer godt sammen med middleware, men middleware-headers påvirker den statiske shell. Undgå at sætte dynamiske headers, der varierer pr. bruger, i middleware på PPR-ruter. Se også den officielle PPR-konfigurationsreference for opdateret information om flaget.
Ofte stillede spørgsmål
Hvad er forskellen mellem Partial Prerendering og Streaming SSR?
Streaming SSR genererer hele HTML'en pr. request, men streamer den ud i chunks, så browseren kan begynde at rendere tidligere. PPR går et skridt videre: den statiske del er allerede genereret ved build-tid og serveres direkte fra CDN, mens kun de dynamiske Suspense-grænser eksekveres pr. request. TTFB er typisk 5-10x hurtigere med PPR end ren streaming SSR.
Kan jeg bruge PPR med React 19?
Ja. Next.js 15.3+ kræver React 19 og er fuldt kompatibel med PPR. React 19's nye use()-hook fungerer fint inden for Suspense-grænser i PPR-ruter og er faktisk den anbefalede måde at læse promises på i streamede komponenter.
Hvordan virker PPR sammen med ISR?
PPR og ISR komplementerer hinanden. Den statiske shell i en PPR-rute kan revalideres med revalidate-export ligesom enhver ISR-side. De dynamiske Suspense-grænser eksekveres altid pr. request og ignoreres af ISR-revalidationen. Dette giver dig kombinationen af friskt cached indhold plus brugerspecifik logik.
Hvilke hostingplatforme understøtter PPR?
Vercel har førsteklasses PPR-support. Cloudflare Workers understøtter PPR via @cloudflare/next-on-pages. Self-hosted Node.js fungerer, så længe din reverse proxy ikke buffer responses. AWS Amplify og Netlify har varierende support, så tjek deres seneste dokumentation.
Er Partial Prerendering dyrere end SSR?
Generelt nej. Den statiske shell serveres fra CDN uden serverkald, hvilket sparer compute. Kun Suspense-grænserne kører på serveren, og de er typisk mindre arbejde end at rendere hele siden. På Vercel ser de fleste teams 30-50% lavere function-invocations-omkostninger ved migration fra ren SSR til PPR.
Komplet guide til Next.js 15 Metadata API i App Router. Lær generateMetadata til dynamisk SEO, dynamiske Open Graph-billeder med next/og, og type-sikre sitemap.ts og robots.ts med kodeeksempler.