Partial Prerendering i Next.js: Komplett guide till PPR och Cache Components (2026)

Så aktiverar du Partial Prerendering (PPR) i Next.js 15 och 16 med rätt Suspense-mönster, felsökning av λ-rutter och migrering till Cache Components. Praktisk guide med kodexempel för e-handel, dashboards och bloggar.

Next.js PPR Guide: Cache Components (2026)

Uppdaterad: 21 juli 2026

Partial Prerendering (PPR) i Next.js är en renderingsstrategi som låter dig kombinera statiskt och dynamiskt innehåll i samma rutt: ett statiskt skal levereras omedelbart från CDN:et medan dynamiska delar strömmas in via Suspense i samma HTTP-svar. Från och med Next.js 16 (släppt 21 oktober 2025) är PPR stabilt under namnet Cache Components, aktiverat via cacheComponents: true. Den här guiden går igenom varför PPR finns, hur du aktiverar det både i Next.js 15 och 16, vilka Suspense-mönster som faktiskt fungerar i produktion, plus de fällor jag har snubblat över själv när jag gick från pages-router till App Router.

  • PPR delar upp en rutt i ett statiskt skal och dynamiska "hål". Skalet cachas på edge, hålen strömmas från origin i samma svar.
  • Allt utanför <Suspense> är statiskt. Allt innanför är dynamiskt. Det är hela mentalmodellen.
  • I Next.js 15 aktiveras PPR med experimental.ppr: 'incremental' plus export const experimental_ppr = true per rutt.
  • I Next.js 16 ersätts flaggan av cacheComponents: true och det nya use cache-direktivet. PPR blir stabilt och opt-in per cachad enhet.
  • Bäst i e-handel, dashboards och innehållssajter med stabilt skal. Undvik för sidor som är 100 % personliga eller ändras på varje begäran.
  • Om hela sidan blir dynamisk (λ istället för i build-outputen) beror det nästan alltid på att cookies(), headers() eller connection() anropas utanför en Suspense-gräns.

Vad är Partial Prerendering i Next.js?

Partial Prerendering är ett renderingsläge där Next.js vid build time producerar ett statiskt HTML-skal för en rutt plus ett "postponed state"-block som beskriver var de dynamiska hålen sitter. Vid request time serveras skalet direkt (ofta från CDN:et) samtidigt som origin-servern renderar de dynamiska delarna och strömmar tillbaka dem i samma HTTP-svar. Slutresultatet är en sida där menyn, hjälte-sektionen och strukturen är synliga på under 100 ms, medan användarspecifika delar (varukorg, notifieringar, personliga rekommendationer) kommer in progressivt.

Innan PPR var App Router binär: ett enda anrop till cookies() eller headers() i rutten gjorde hela sidan dynamisk. Det innebar att en enda personaliserad widget kunde slå ut den statiska optimeringen för en produktsida med 10 000 SKU. Honestly, det var en av de största frustrationerna jag hörde från teamet när vi migrerade vår första större kund. PPR bryter det tvånget genom att låta dynamiska anrop leva inne i Suspense-gränser utan att smitta det yttre skalet. Enligt Next.js 16-releasen ger kombinationen typiskt 60–80 % lägre TTFB jämfört med en helt dynamisk sida.

Från Next.js 16 heter samma primitiv Cache Components, men den underliggande semantiken är oförändrad: allt som inte cachas explicit är dynamiskt, och blandning inom samma rutt hanteras via PPR under huven. För oss som skrivit Next.js sedan pages-only-dagarna är det ungefär motsvarigheten till att kunna använda getStaticProps och getServerSideProps i samma fil, bara att gränsen är Suspense istället för filen.

Hur fungerar PPR i praktiken?

Renderingsprocessen sker i två faser. Under build time går Next.js igenom komponentträdet och stöter på Suspense-gränser. Allt som ligger utanför en gräns renderas till statisk HTML. När den träffar en gräns med en dynamisk komponent inuti, pausar den ("postpones") det subträdet och sparar tillräckligt med state för att kunna återuppta renderingen senare. Fallback-innehållet (skelettet du skickar till fallback-propen) läggs in i det statiska skalet.

Under request time händer följande i sekvens: CDN:et returnerar det cachade skalet omedelbart, en resume-request skickas parallellt till origin-servern, origin renderar bara de dynamiska hålen med aktuell request-context (cookies, headers, URL-parametrar), och CDN:et sammanfogar skalet och de strömmade dynamiska bitarna till ett enda strömmande HTTP-svar. Från klientens sida ser det ut som ett vanligt svar, bara att TTFB är dramatiskt kortare.

Det som gör det här arkitektoniskt intressant är felhanteringen. Om ett dynamiskt hål misslyckas (API-timeout, 500 från databasen) fångas felet av Suspense-gränsens error.tsx. Skalet är redan levererat, så användaren ser en graciöst degraderad sida med statiskt innehåll intakt och bara det trasiga hålet visande ett felmeddelande. Jämför det med klassisk SSR där ett enda misslyckat upstream-anrop ger 500 för hela sidan. Efter att ha kört pages-router-appar med getServerSideProps i fem år är detta ensamt värt migreringen.

Så aktiverar du PPR i Next.js 15

I Next.js 15 är PPR fortfarande experimentell och aktiveras i två steg. Först i next.config.ts:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    ppr: 'incremental',
  },
}

export default nextConfig

Värdet 'incremental' är viktigt: det låter dig opta in per rutt istället för att slå på PPR globalt. Global aktivering (ppr: true) fungerar men innebär att varje rutt måste vara PPR-kompatibel, vilket sällan är realistiskt i en migrering från pages-router. Jag rekommenderar starkt att köra 'incremental' under första kvartalet av migreringen.

Andra steget är att opta in per rutt genom att exportera experimental_ppr från en layout eller sida:

// app/products/layout.tsx
export const experimental_ppr = true

export default function ProductsLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <section>{children}</section>
}

Exporten propagerar nedåt: alla barn-rutter (inklusive nästlade layouter och sidor) ärver PPR-flaggan. Du behöver alltså inte upprepa exporten i varje fil, bara den översta segmentet du vill aktivera. Det gör det praktiskt att inkrementellt rulla ut PPR. Börja med /products, verifiera i produktion, gå vidare till /blog, och så vidare.

Verifiera att aktiveringen fungerar genom att köra next build. I outputen ska rutter med PPR markeras med (halv-cirkel) istället för (fullt statisk) eller λ (fullt dynamisk). Om du ser λ där du förväntade dig , se avsnittet om felsökning nedan.

Suspense-gränser: kärnmönstret för PPR

Hela poängen med PPR är gränsen mellan statiskt och dynamiskt, och den gränsen är <Suspense>. Regeln är lika enkel som avgörande: allt utanför en Suspense-gräns är statiskt, allt inuti är dynamiskt. Här är ett realistiskt exempel för en produktsida:

// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { ProductDetails } from './product-details'
import { RecommendedProducts } from './recommended-products'
import { UserCartPreview, CartSkeleton } from './cart-preview'
import { LiveInventory, InventorySkeleton } from './inventory'

export const experimental_ppr = true

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  return (
    <article>
      {/* Statiskt: renderas vid build, cachas på edge */}
      <ProductDetails id={id} />
      <RecommendedProducts productId={id} />

      {/* Dynamiskt: strömmas från origin per begäran */}
      <Suspense fallback={<InventorySkeleton />}>
        <LiveInventory productId={id} />
      </Suspense>

      <Suspense fallback={<CartSkeleton />}>
        <UserCartPreview />
      </Suspense>
    </article>
  )
}

Notera att ProductDetails och RecommendedProducts ligger utanför Suspense. Det innebär att de måste vara helt statiskt genererbara. De får inte anropa cookies(), headers() eller läsa searchParams. Om ProductDetails gör en fetch, måste den fetchen vara cachbar. UserCartPreview läser däremot en cookie för att slå upp användarens varukorg, och det är helt okej så länge det anropet sker inuti Suspense-gränsen.

Fallback-komponenten bör vara ett realistiskt skelett, inte en spinner. Kom ihåg att fallback:en är den HTML som visas i det statiska skalet innan den dynamiska datan anländer. Skelett med rätt dimensioner ger en stabil layout och undviker Cumulative Layout Shift när det riktiga innehållet ersätter placeholdern. Det här är samma tänk som jag redan använde i pages-router med react-loading-skeleton, bara att nu är det inbakat i renderingsmodellen.

När ska du använda PPR (och när inte)?

PPR är inte universellt bättre. Den rätta frågan är alltid: "Har den här sidan ett stabilt statiskt skal med isolerade dynamiska sektioner?" När svaret är ja är PPR en enorm vinst. När svaret är nej lönar det sig att stanna kvar i antingen fullt dynamisk rendering eller ren SSG.

Bra kandidater för PPR

  • Produktsidor i e-handel: stabil layout, priset kan vara statiskt, lagerstatus och användarvarukorg är dynamiska hål.
  • Blogg- och innehållssajter med kommentarer: artikeln själv cachas, kommentar-tråden och engagemangsmetriker strömmas.
  • Dashboards med publikt skal: navigation och layout statiska, widgets per användare i Suspense.
  • Landningssidor med personaliserade element: hjälte- och funktionssektioner statiska, "rekommenderat till dig"-sektion dynamisk.

Undvik PPR när...

  • Sidan är 100 % personlig (kontoinställningar, admin-dashboards). Här finns inget skal att cacha, så spara komplexiteten och kör fullt dynamiskt.
  • Innehållet ändras på varje begäran ändå (live-feeds, realtidsövervakning). Det finns inget statiskt att prerendera.
  • Skalet är trivialt jämfört med hålet. Om 95 % av sidan är dynamisk vinner du nästan inget i TTFB.
  • Du kör en Next.js middleware som redan personaliserar responsen tungt. Kombinationen kan bli svår att resonera om.

En annan värdefull faktor är teamets komfort med Suspense-modellen. Om ditt team fortfarande brottas med "loading.tsx vs Suspense vs error boundaries" är det värt att stabilisera de mönstren först innan PPR läggs på. I mitt förra projekt såg vi själva bättre resultat genom att lära ut Suspense först i en månad och sedan aktivera PPR, inte tvärtom.

Varför blir min sida helt dynamisk?

Den vanligaste frustrationen med PPR är att build-outputen visar λ istället för . Sidan opterade ut från prerendering. Orsaken är nästan alltid att ett dynamiskt API anropas utanför en Suspense-gräns. Vanliga misstänkta:

  • cookies() eller headers() i en layout eller sidkomponent utanför Suspense.
  • Läsning av searchParams i page.tsx. Dessa är en Promise i Next.js 15+ och att awaita dem gör rutten dynamisk.
  • Ett fetch()-anrop med cache: 'no-store' eller next.revalidate: 0 i det statiska skalet.
  • connection()-anrop (som signalerar att du behöver den aktuella requestens kontext).
  • Ett tredjeparts-SDK som internt läser headers eller cookies.

Felsök detta systematiskt genom att köra en verbose build:

NEXT_LOG_LEVEL=debug next build

I outputen får du rader som "Route /dashboard is dynamic because cookies() was called in /components/UserAvatar.tsx". Det talar exakt om vilken fil som är boven. Flytta anropet in i en komponent som ligger inne i en Suspense-gräns, eller om det är oundvikligt: acceptera att rutten är dynamisk och skippa PPR för just den vyn. Jag hade en riktig aha-stund här när jag insåg att ett Sentry-SDK i vår root layout tyst gjorde varje rutt dynamisk.

Kombinera PPR med ISR och revalidering

PPR ersätter inte Incremental Static Regeneration. De kompletterar varandra. Det statiska skalet i en PPR-rutt kan revalideras precis som vilken statisk sida som helst, medan de dynamiska hålen alltid är färska. Praktiskt exempel för en bloggpost:

// app/blog/[slug]/page.tsx
import { Suspense } from 'react'
import { PostContent } from './post-content'
import { Comments, CommentsSkeleton } from './comments'
import { LiveReactionCount } from './reactions'

export const experimental_ppr = true
export const revalidate = 3600 // ISR: regenerera skalet varje timme

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetchPost(slug) // cachbar fetch

  return (
    <article>
      <PostContent post={post} />

      <Suspense fallback={<span>–</span>}>
        <LiveReactionCount postId={post.id} />
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={post.id} />
      </Suspense>
    </article>
  )
}

Här händer flera saker samtidigt: bloggposten är statiskt genererad och regenereras varje timme via ISR, reaktionsräknaren och kommentarerna är dynamiska hål som strömmas per begäran, och hela svaret är ett enda streamat HTTP-svar. Läs mer om revalideringsmodellen i vår guide till Next.js 15 cachning med fetch och revalidateTag. Särskilt avsnittet om revalidateTag() är relevant när du vill invalidera det statiska skalet från en Server Action.

Next.js 16 och Cache Components

Med Next.js 16-releasen 21 oktober 2025 blev PPR stabilt, men under ett nytt namn och en delvis omdesignad API. Den experimentella experimental.ppr-flaggan ersätts av cacheComponents: true, och det nya use cache-direktivet blir det primära sättet att markera cachbara enheter.

// next.config.ts (Next.js 16+)
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

Filosofin flippar helt. Från "allt cachas som standard och du optar ut med no-store" till "allt är dynamiskt som standard och du optar in med use cache". Det gör mentalmodellen enklare, eftersom du vet exakt vad som cachas när du har markerat det själv.

// app/products/[id]/product-details.tsx
'use cache'

import { cacheTag } from 'next/cache'

export async function ProductDetails({ id }: { id: string }) {
  cacheTag(`product:${id}`)

  const product = await db.products.findUnique({ where: { id } })

  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.description}</p>
    </div>
  )
}

Direktivet use cache kan appliceras på tre nivåer: fil (markerar varje export som cachad), funktion (wrappar en individuell async-funktion) eller komponent (wrappar en Server Component). Rekommendationen från Vercels team är att börja med funktionsnivå vid migrering. Det ger finkornig kontroll och är lätt att rulla tillbaka om en specifik cache-nyckel beter sig oväntat. Se den officiella PPR Platform Guide för fullständiga scopingregler.

Rutter som blandar cachade och icke-cachade segment blir automatiskt partiellt prerenderade. Det finns ingen separat "PPR-flagga" längre, det är helt enkelt effekten av att ha både use cache-markerade och dynamiska komponenter i samma träd.

Migrera från experimental_ppr till cacheComponents

Migreringen från Next.js 15 med experimental.ppr till Next.js 16 med cacheComponents är oftast enklare än den låter. Det första steget är att köra codemod:

npx @next/codemod@canary next-async-request-api .
npx @next/codemod@canary next-16-cache-components .

Codemod-verktyget hanterar det mekaniska. Det byter experimental.ppr mot cacheComponents, tar bort experimental_ppr-exporter från rutterna, och lägger till use cache-direktiv på ställen där det tydligt kan härledas. Det som kräver manuellt arbete är att bestämma cache-scope för varje statisk komponent. Bör den vara cachad på filnivå, funktionsnivå eller komponentnivå?

En annan förändring att vara medveten om är att unstable_cache är formellt deprecated i Next.js 16. Om din pages-router-migrering använde unstable_cache för att memoize icke-fetch-data (databasfrågor, tredjeparts-SDK-svar), är migrationsvägen till use cache-direktivet. Codemoden markerar dessa ställen med kommentarer så du kan gå igenom dem manuellt. Läs Next.js 16 release-noterna för hela listan över deprecations, och kika gärna på det officiella Next.js GitHub-repot om du vill följa aktuella diskussioner kring cache-scopingen.

Om du inte är redo att flytta till Cache Components-modellen direkt, är alternativet att pinna dig på Next.js 15 tills tredje kvartalet 2026 då LTS-supporten fortfarande täcker den. På lång sikt är riktningen tydlig: cachning i Next.js är nu opt-in, explicit och komponerbar, och det är dit hela ekosystemet är på väg.

Vanliga frågor

Är Partial Prerendering stabilt i Next.js 16?

Ja. Från och med Next.js 16 (21 oktober 2025) är PPR stabilt under namnet Cache Components. Den experimentella flaggan experimental.ppr ersätts av cacheComponents: true i next.config.ts, och per-rutt-exporten experimental_ppr försvinner till förmån för use cache-direktivet.

Vad är skillnaden mellan PPR och SSR?

SSR renderar hela sidan på servern per begäran, så TTFB skalar med den långsammaste datakällan. PPR levererar ett statiskt skal från CDN:et omedelbart och strömmar bara de dynamiska delarna från origin. Resultatet: bättre TTFB och graciös felhantering, eftersom en misslyckad datahämtning bara påverkar sitt eget hål istället för hela sidan.

Fungerar PPR med pages-router?

Nej. PPR är exklusivt för App Router eftersom det bygger på React Server Components och Suspense-strömning som pages-router inte stöder. Om du har en pages-router-applikation som skulle dra nytta av PPR är migrationsvägen först till App Router, sedan aktivering av PPR (eller Cache Components i Next.js 16+).

Kan jag använda PPR med middleware?

Ja, men var försiktig. Om din middleware skriver headers eller cookies som Server Components i skalet läser, riskerar det statiska skalet att bli dynamiskt. Läs cookies bara i komponenter som ligger inne i Suspense-gränser, så förblir skalet cachbart. Från Next.js 16 heter middleware-motsvarigheten proxy.ts.

Hur felsöker jag när PPR inte aktiveras?

Kör NEXT_LOG_LEVEL=debug next build. Outputen listar exakt vilken fil och vilket API-anrop som gjorde rutten dynamisk. Vanligaste orsakerna: cookies() eller headers() utanför en Suspense-gräns, searchParams som awaitas i sidkomponenten, eller en fetch med cache: 'no-store' i skalet.

Ben Howard
Om Författaren Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.