Next.js 15 fullstack (App Router + Server Actions) · leksjon

Delvis forhåndsgjengivelse: statisk skall og dynamiske hull

Kombiner et forhåndsgjengitt statisk skall med strømmede dynamiske områder ved hjelp av PPR-modellen.

Leksjon 3 av 413 trinn

Delvis forhåndsgjengivelse: statisk skall og dynamiske hull er en gratis leksjon i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit. Dette er leksjon 3 av 4. Du kan lese valgfritt 3 leksjoner fra denne læringsstien gratis i sin helhet – deretter låser CoddyKit PRO opp alle leksjoner, samt praktisk øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Next.js 15 fullstack (App Router + Server Actions), og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Next.js 15 fullstack (App Router + Server Actions) inneholder totalt 4 leksjoner.

Hva er delvis forhåndsgjengivelse?

Partial Prerendering (PPR) er en gjengivelsesmodell som ble introdusert i Next.js 14+ og videreutviklet i Next.js 15. Den gjør det mulig for én enkelt rute å levere både statisk og dynamisk innhold samtidig.

Med tradisjonell gjengivelse måtte du velge én av to løsninger:

  • Statisk (SSG/SSR med full hurtigbuffer) — raskt, men utdatert for personaliserte data
  • Dynamisk (SSR ved hver forespørsel) — ferskt, men med lengre tid til første byte

PPR fjerner denne avveiningen. Ved bygging forhåndsgjengir Next.js et statisk skall (oppsett, rammeverk og innhold over bretten), og lager hull der dynamiske data kan strømmes inn når forespørselen behandles — alt fra én rutefil.

Aktivere PPR i Next.js 15

PPR er en funksjon du må velge å aktivere. Aktiver den i next.config.ts med flagget experimental.ppr.

I Next.js 15 kan du også aktivere den trinnvis — per rute — ved å bruke konfigurasjonseksporten experimental_ppr for rutesegmentet, i stedet for å slå den på globalt.

Når funksjonen er aktivert, analyserer rammeverket rutetreet ved bygging og skiller det statiske skallet fra deltrær som inneholder dynamiske API-er (cookies(), headers(), searchParams, noStore() osv.).

// 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;

Aktivere PPR trinnvis for en rute

Når du setter ppr: 'incremental' i next.config.ts, må individuelle rutesegmenter uttrykkelig velge dette ved å eksportere konstanten experimental_ppr satt til true.

Dette passer godt når store apper skal migreres gradvis — bare sidene du merker, får PPR-behandling, mens resten oppfører seg som før.

Eksporten må ligge direkte i side- eller layoutfilen til segmentet du vil forhåndsgjengi delvis.

// 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>
  );
}

Det statiske skallet

Det statiske skallet er alt i ruten som ikke avhenger av dynamiske API-er. Next.js forhåndsgjengir og hurtigbufrer dette ved bygging (klart for CDN), slik at det leveres umiddelbart — før et eneste databasekall utføres.

Typisk innhold i det statiske skallet er:

  • Sideoppsett og navigasjonsrammeverk
  • Hero-seksjoner med hardkodet eller statisk hentet tekst
  • Skjelett- og plassholdergrensesnitt for de dynamiske hullene
  • Server Components som bare leser fra filsystemet eller bruker statisk fetch med full hurtigbufring

Det viktige poenget er at det statiske skallet leveres som en forhåndsgjengitt HTML-strøm, mens de dynamiske hullene fortsetter å lastes inn parallelt.

// 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>
  );
}

Definere dynamiske hull med Suspense

Et dynamisk hull er et deltre som er omsluttet av en <Suspense>-grense og inneholder komponenter som bruker dynamiske Next.js-API-er.

Next.js bruker Suspense-grensen som skillet mellom statisk og dynamisk. Alt innenfor grensen utelates fra det forhåndsgjengitte skallet og strømmes i stedet som en separat del når dataene er klare.

Viktige regler:

  • Den dynamiske Server Component-en må være den async-komponenten som utfører datahentingen — ikke bare et omslag
  • Du må angi en fallback for Suspense-grensen; denne reservevisningen bygges inn i det statiske skallet, slik at brukerne ser den umiddelbart
  • Flere uavhengige Suspense-grenser oppretter flere uavhengige hull som strømmes parallelt
// 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 }] };
}

Flere uavhengige dynamiske hull

En av de største fordelene med PPR er at du kan ha flere uavhengige dynamiske hull i én enkelt rute. Hver Suspense-grense strømmer innholdet sitt uavhengig, slik at et tregt databasekall i ett område ikke blokkerer et annet.

Se for deg et kontrollpanel med:

  • En brukerhilsen (leser cookies())
  • En widget med sanntidsstatistikk (leser fra en treg analysedatabase)
  • En feed med nylig aktivitet (leser fra en annen tabell)

Alle tre strømmes parallelt etter at det statiske skallet er levert. Resultatet er en gradvis forbedret brukeropplevelse i stedet for én lang ventetid.

// 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>
  );
}

Hva gjør en komponent dynamisk?

Next.js oppdager automatisk dynamisk bruk ved å lete etter bestemte API-er. En Server Component regnes som dynamisk (og utelates fra det statiske skallet) når den bruker:

  • cookies() eller headers() fra next/headers
  • searchParams-propen på en side
  • unstable_noStore() eller fetch med cache: 'no-store'
  • Et API som deaktiverer Data Cache ved behandling av forespørselen

Hvis et dynamisk API brukes utenfor en Suspense-grense, går hele ruten over til full dynamisk gjengivelse — PPR deaktiveres for denne ruten. Omslutt alltid dynamiske komponenter med Suspense for å bevare det statiske skallet.

// 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' };
}

Skjelett-reservevisninger er en del av det statiske skallet

fallback-propen for hver Suspense-grense inkluderes i det forhåndsgjengitte statiske skallet. Det betyr at brukerne mottar skjelettgrensesnittet uten forsinkelse — med samme latenstid som for en helt statisk side.

Utform skjelettene slik at de samsvarer tett med dimensjonene og oppsettet til det endelige innholdet. Da minimerer du Cumulative Layout Shift (CLS) når det dynamiske innholdet strømmes inn og erstatter skjelettet.

En god skjelettstrategi:

  • Tilpass bredden og høyden til det faktiske innholdet så nøyaktig som mulig
  • Bruk CSS-animasjoner (pulse/shimmer) for å vise at innholdet lastes
  • Unngå plassholdere som ser ut som faktiske data — brukerne kan tolke dem som innhold
// 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 kontra tradisjonell strømmet SSR

Det er lett å forveksle PPR med vanlig strømmet SSR (Suspense uten PPR). Her er den viktigste forskjellen:

  • Strømmet SSR (uten PPR) — Hele svaret gjengis på serveren når forespørselen behandles. De statiske delene gjengis raskt, mens de dynamiske delene strømmes senere. Den innledende HTML-en hurtigbufres imidlertid aldri ved CDN-kanten — hver forespørsel treffer serveren.
  • PPR — Det statiske skallet forhåndsgjengis ved bygging og leveres umiddelbart fra CDN-kanten. Bare de dynamiske hullene utløser kjøring på serveren når forespørselen behandles. Du får både statisk levering med CDN-hastighet og ferske dynamiske data.

PPR er i praksis: statisk levering fra CDN + strømming ved forespørsel, kombinert i én rute.

Lese searchParams uten å bryte PPR

I Next.js 15 er searchParams på en Page-komponent en dynamisk verdi — når du leser den, velger hele siden bort statisk gjengivelse. Med PPR må du ikke lese searchParams direkte i sidekomponenten (utenfor en Suspense-grense).

Send i stedet searchParams som en prop inn i en komponent som er omsluttet av Suspense. Da forblir det ytre sideskallet statisk, samtidig som den indre komponenten kan lese spørringsparametere fra den aktuelle forespørselen.

// 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>
  );
}

Kontrollere PPR-oppførsel under utvikling

Under next dev simuleres PPR, men sidene forhåndsbygges ikke i praksis – hver forespørsel gjengis på nytt. For å se hvordan PPR faktisk fungerer, må De kjøre et produksjonsbygg:

  • next build – genererer det statiske skallet ved byggetid
  • next start – leverer produksjonsresultatet

Etter et bygg kan De se på resultattabellen som skrives ut i terminalen. Ruter med PPR er merket med symbolet ◐ (halvfylt sirkel), som angir delvis forhåndsgjengivelse – forskjellig fra helt statiske (○) og helt dynamiske (λ) ruter.

De kan også undersøke mappen .next/server/app for den forhåndsgjengitte HTML-filen og den separate RSC-nyttelasten for de dynamiske hullene.

Kunnskapssjekk: Regler for PPR-grenser

En utvikler aktiverer PPR for en rute og pakker den dynamiske komponenten inn i en Suspense-grense. Hvilket av følgende utsagn beskriver korrekt hva som skjer hvis utvikleren kaller cookies() direkte i Page-komponenten (utenfor en Suspense-grense)?

Oppsummering: Delvis forhåndsgjengivelse i praksis

I denne leksjonen har De utforsket Partial Prerendering (PPR) – modellen i Next.js 15 som kombinerer statisk levering med CDN-hastighet og dynamisk strømming på forespørselstidspunktet.

Viktig å ta med seg:

  • Aktiver PPR globalt via experimental.ppr: true i next.config.ts, eller per rute med export const experimental_ppr = true
  • Det statiske skallet (alt utenfor Suspense-grenser) forhåndsgjengis ved byggetid og leveres umiddelbart fra CDN-et
  • Hver Suspense-grense definerer et dynamisk hull – reserveinnholdet bygges inn i skallet, mens innholdet strømmes inn på forespørselstidspunktet
  • Dynamiske API-er (cookies(), headers(), searchParams, no-store-forespørsler) må ligge inne i Suspense-grenser for å bevare skallet
  • Flere uavhengige Suspense-grenser strømmes inn parallelt, noe som maksimerer den opplevde ytelsen
  • Bygg med next build og se etter symbolet ◐ for å bekrefte at PPR er aktivert for en rute

PPR er broen mellom det alt-eller-ingenting-valget man ellers har mellom statisk og dynamisk innhold – bruk det til å levere raske skall og ferske data fra den samme rutefilen.

Gratis å komme i gang

Lær deg TypeScript med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
88

Ofte stilte spørsmål

Er leksjonen «Delvis forhåndsgjengivelse: statisk skall og dynamiske hull» gratis?

Ja – du kan lese valgfritt 3 av leksjonene i læringsstien Next.js 15 fullstack (App Router + Server Actions), inkludert «Delvis forhåndsgjengivelse: statisk skall og dynamiske hull», gratis i sin helhet her på nettet. Deretter låser CoddyKit PRO opp alle leksjoner, samt interaktiv øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Kurset i Next.js 15 fullstack (App Router + Server Actions) inneholder totalt 4 leksjoner.

Hva lærer jeg i «Delvis forhåndsgjengivelse: statisk skall og dynamiske hull»?

Kombiner et forhåndsgjengitt statisk skall med strømmede dynamiske områder ved hjelp av PPR-modellen. Du øver på Next.js 15 fullstack (App Router + Server Actions) med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Next.js 15 fullstack (App Router + Server Actions)?

Ingen tidligere erfaring er nødvendig. Next.js 15 fullstack (App Router + Server Actions) på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 3 av 4.

Hvor lang tid tar leksjonen «Delvis forhåndsgjengivelse: statisk skall og dynamiske hull»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Next.js 15 fullstack (App Router + Server Actions)-leksjonen?

Ja. Alle Next.js 15 fullstack (App Router + Server Actions)-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Suspense-grenser og strømming på komponentnivå
  2. Utforme meningsfulle loading.tsx-filer og skeletons
  3. Delvis forhåndsgjengivelse: statisk skall og dynamiske hull
  4. Fallgruver ved strømming: layoutforskyvning og vannfall
← Tilbake til Next.js 15 fullstack (App Router + Server Actions)