Next.js 15 -fullstack-kehitys (App Router + Server Actions) · Oppitunti

Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot

Yhdistäkää esirenderöity staattinen runko suoratoistettaviin dynaamisiin alueisiin PPR-mallilla.

Oppitunti 3/413 vaihetta

Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot on ilmainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Next.js 15 -fullstack-kehitys (App Router + Server Actions)-kurssilla on yhteensä 4 oppituntia.

Mitä osittainen esirenderöinti on?

Partial Prerendering (PPR) on Next.js 14+:ssa käyttöön otettu ja Next.js 15:ssä kehitetty renderöintimalli, jonka avulla yksi reitti voi tarjota sekä staattista että dynaamista sisältöä samanaikaisesti.

Perinteisessä renderöinnissä oli valittava toinen vaihtoehto:

  • Staattinen (SSG/SSR täydellä välimuistilla) — nopea, mutta personoitujen tietojen osalta vanhentunut
  • Dynaaminen (SSR jokaisella pyynnöllä) — ajantasainen, mutta ensimmäisen tavun vastaanottoaika (Time-To-First-Byte) on pidempi

PPR poistaa tämän kompromissin. Koontivaiheessa Next.js esirenderöi staattisen kuoren (asettelu, käyttöliittymän kehys ja ensimmäisen näkymän sisältö) ja jättää aukkoja kohtiin, joihin dynaaminen data striimataan pyynnön aikana — kaikki yhdestä reittitiedostosta.

PPR:n käyttöönotto Next.js 15:ssä

PPR on erikseen käyttöön otettava ominaisuus. Ottakaa se käyttöön tiedostossa next.config.ts lipulla experimental.ppr.

Next.js 15:ssä voitte ottaa sen käyttöön myös vaiheittain reittikohtaisesti käyttämällä experimental_ppr-reittisegmentin asetuksen vientiä sen sijaan, että ottaisitte ominaisuuden käyttöön kaikkialla.

Kun ominaisuus on käytössä, kehys analysoi reittipuun koontivaiheessa ja erottaa staattisen kuoren alipuista, jotka sisältävät dynaamisia rajapintoja (cookies(), headers(), searchParams, noStore() jne.).

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

Reitin ottaminen asteittain PPR:n piiriin

Kun asetatte ppr: 'incremental' tiedostossa next.config.ts, yksittäisten reittisegmenttien on liityttävä ominaisuuteen erikseen viemällä vakio experimental_ppr arvolla true.

Tämä sopii erinomaisesti suurten sovellusten asteittaiseen siirtämiseen — vain merkitsemänne sivut käsitellään PPR:n mukaisesti, kun taas muut toimivat entiseen tapaan.

Viennin on sijaittava suoraan sen segmentin sivu- tai asettelutiedostossa, jonka haluatte esirenderöidä osittain.

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

Staattinen kuori

Staattinen kuori sisältää kaiken reitin sisällön, joka ei riipu dynaamisista rajapinnoista. Next.js esirenderöi ja tallentaa sen välimuistiin koontivaiheessa (CDN-valmis), joten se toimitetaan heti — ennen kuin yksikään tietokantakutsu suoritetaan.

Staattinen kuori sisältää tyypillisesti seuraavat:

  • Sivun asettelu ja navigoinnin kehys
  • Hero-osiot, joiden teksti on kovakoodattu tai haettu staattisesti
  • Dynaamisten aukkojen skeleton- ja paikanvaraajakäyttöliittymä
  • Server Components -komponentit, jotka lukevat vain tiedostojärjestelmästä tai täydellä välimuistilla tehdystä staattisesta fetch-kutsusta

Keskeinen ajatus on, että staattinen kuori toimitetaan esirenderöitynä HTML-striiminä, samalla kun dynaamiset aukot latautuvat edelleen rinnakkain.

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

Dynaamisten aukkojen määrittäminen Suspensella

Dynaaminen aukko on alipuu, joka on ympäröity <Suspense>-rajalla ja sisältää dynaamisia Next.js-rajapintoja käyttäviä komponentteja.

Next.js käyttää Suspense-rajaa staattisen ja dynaamisen sisällön välisenä rajana. Kaikki rajan sisällä oleva jätetään pois esirenderöidystä kuoresta ja striimataan erillisenä osana, kun data on valmis.

Tärkeät säännöt:

  • Dynaamisen Server Component -komponentin on oltava datahaun tekevä async-komponentti — pelkkä kääre ei riitä
  • Suspense-rajalle on annettava fallback; tämä varasisältö sisällytetään staattiseen kuoreen, joten käyttäjät näkevät sen heti
  • Useat toisistaan riippumattomat Suspense-rajat luovat useita toisistaan riippumattomia aukkoja, jotka striimataan rinnakkain
// 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 }] };
}

Useita toisistaan riippumattomia dynaamisia aukkoja

Yksi PPR:n suurimmista eduista on, että yhdellä reitillä voi olla useita toisistaan riippumattomia dynaamisia aukkoja. Jokainen Suspense-raja striimaa sisältönsä itsenäisesti, joten yhden alueen hidas tietokantakutsu ei estä toista aluetta.

Ajatelkaa hallintapaneelia, jossa on:

  • käyttäjän tervehdys (lukee cookies()-arvon)
  • reaaliaikainen tilastowidget (lukee hitaasta analytiikkatietokannasta)
  • viimeisimpien tapahtumien syöte (lukee eri taulusta)

Kaikki kolme striimataan rinnakkain staattisen kuoren toimittamisen jälkeen. Näin käyttäjäkokemus paranee vaiheittain yhden pitkän odotuksen sijaan.

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

Mikä tekee komponentista dynaamisen?

Next.js tunnistaa dynaamisen käytön automaattisesti etsimällä tiettyjä rajapintoja. Server Component katsotaan dynaamiseksi (ja jätetään staattisen kuoren ulkopuolelle), kun se käyttää seuraavia:

  • next/headers-moduulin cookies()- tai headers()-funktiota
  • sivun searchParams-proppia
  • unstable_noStore()-funktiota tai fetch-kutsua asetuksella cache: 'no-store'
  • mitä tahansa rajapintaa, joka poistaa Data Cachen käytöstä pyynnön aikana

Jos dynaamista rajapintaa käytetään Suspense-rajan ulkopuolella, koko reitti siirtyy täysin dynaamiseen renderöintiin — PPR poistetaan käytöstä kyseiseltä reitiltä. Ympäröikää dynaamiset komponentit aina Suspensella, jotta staattinen kuori säilyy.

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

Skeleton-varasisällöt ovat osa staattista kuorta

Jokaisen Suspense-rajan fallback-proppi sisällytetään esirenderöityyn staattiseen kuoreen. Käyttäjät saavat siis skeleton-käyttöliittymän ilman viivettä — samalla viiveellä kuin täysin staattisen sivun.

Suunnitelkaa skeletonit niin, että ne vastaavat mahdollisimman tarkasti lopullisen sisällön mittoja ja asettelua. Tämä vähentää kumulatiivista asettelun siirtymää (CLS), kun dynaaminen sisältö striimataan ja se korvaa skeletonin.

Hyvä skeleton-strategia:

  • Sovittakaa todellisen sisällön leveys ja korkeus mahdollisimman tarkasti yhteen
  • Käyttäkää CSS-animaatioita (pulse/shimmer) lataustilan ilmaisemiseen
  • Välttäkää paikanvaraajia, jotka näyttävät oikealta datalta — käyttäjät voivat tulkita ne sisällöksi
// 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 ja perinteinen striimaava SSR

PPR sekoitetaan helposti tavalliseen striimaavaan SSR:ään (jossa käytetään Suspensea ilman PPR:ää). Tässä keskeinen ero:

  • Striimaava SSR (ei PPR:ää) — Koko vastaus renderöidään palvelimella pyynnön aikana. Staattiset osat renderöityvät nopeasti ja dynaamiset osat striimataan myöhemmin. Alkuperäistä HTML:ää ei kuitenkaan koskaan tallenneta CDN-reunalle välimuistiin — jokainen pyyntö osuu palvelimelle.
  • PPR — Staattinen kuori esirenderöidään koontivaiheessa ja toimitetaan CDN-reunalta heti. Vain dynaamiset aukot käynnistävät palvelimen suorituksen pyynnön aikana. Saatte sekä CDN-nopean staattisen toimituksen että tuoreen dynaamisen datan.

PPR tarkoittaa käytännössä seuraavien yhdistelmää: staattisen sisällön toimitus CDN:ltä + striimaus pyynnön aikana yhdellä reitillä.

searchParams-arvon lukeminen rikkomatta PPR:ää

Next.js 15:ssä Sivukomponentin searchParams on dynaaminen arvo — sen lukeminen poistaa koko sivun staattisen renderöinnin piiristä. PPR:n yhteydessä searchParams-arvoa ei saa lukea suoraan sivukomponentissa (Suspense-rajan ulkopuolella).

Välittäkää sen sijaan searchParams proppina Suspensella ympäröidylle komponentille. Näin ulompi sivun kuori säilyy staattisena, mutta sisempi komponentti voi lukea pyynnön aikaiset kyselyparametrit.

// 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:n toiminnan tarkistaminen kehitystilassa

next dev -tilassa PPR:ää simuloidaan, mutta sivuja ei esirakenneta oikeasti — jokainen pyyntö käynnistää uuden renderöinnin. Voit tarkastella PPR:n todellista toimintaa suorittamalla tuotantoversion koontiversion:

  • next build — luo staattisen kuoren koontivaiheessa
  • next start — tarjoaa tuotantoversion sisällön

Tarkista koontiversion jälkeen päätelaitteeseen tulostettu tulostaulukko. PPR:ää käyttävät reitit on merkitty symbolilla ◐ (puoliksi täytetty ympyrä), joka ilmaisee osittaisen esirenderöinnin — tämä poikkeaa täysin staattisista (○) ja täysin dynaamisista (λ) reiteistä.

Voit myös tarkastella .next/server/app-hakemistoa nähdäksesi esirenderöidyn HTML-tiedoston ja dynaamisia aukkoja varten erillisen RSC-hyötykuorman.

Tietotesti: PPR:n rajojen säännöt

Kehittäjä ottaa PPR:n käyttöön reitillä ja ympäröi dynaamisen komponenttinsa Suspense-rajalla. Mikä seuraavista väitteistä kuvaa oikein, mitä tapahtuu, jos kehittäjä kutsuu cookies()-funktiota suoraan Page-komponentin sisällä (minkään Suspense-rajan ulkopuolella)?

Kertaus: osittainen esirenderöinti käytännössä

Tässä oppitunnissa tutustuit osittaiseen esirenderöintiin (Partial Prerendering, PPR) — Next.js 15:n malliin, jossa CDN:n nopea staattinen jakelu yhdistyy pyynnön aikana tapahtuvaan dynaamiseen suoratoistoon.

Tärkeimmät opit:

  • Ota PPR käyttöön maailmanlaajuisesti määrittämällä experimental.ppr: true tiedostossa next.config.ts tai reittikohtaisesti määrittämällä export const experimental_ppr = true
  • Staattinen kuori (kaikki Suspense-rajojen ulkopuolinen) esirenderöidään koontivaiheessa ja tarjotaan CDN:stä välittömästi
  • Jokainen Suspense-raja määrittää dynaamisen aukon — sen varasisältö sisällytetään kuoreen ja varsinainen sisältö suoratoistetaan pyynnön aikana
  • Dynaamisten API:en (cookies(), headers(), searchParams, no-store-hakujen) on sijaittava Suspense-rajojen sisäpuolella, jotta kuori säilyy
  • Useat toisistaan riippumattomat Suspense-rajat suoratoistetaan rinnakkain, mikä maksimoi koetun suorituskyvyn
  • Luo koontiversio komennolla next build ja etsi symboli ◐ varmistaaksesi, että PPR on aktiivinen reitillä

PPR yhdistää staattisen ja dynaamisen toteutuksen, joita aiemmin oli pidettävä joko–tai-valintana — käytä sitä nopeiden kuorien ja ajantasaisten tietojen tarjoamiseen samasta reittitiedostosta.

Aloita maksutta

Opi TypeScript tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
22
Oppitunnit
88

Usein kysytyt kysymykset

Onko oppitunti ”Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolun 3 oppituntia, myös oppitunnin “Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Next.js 15 -fullstack-kehitys (App Router + Server Actions)-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot”?

Yhdistäkää esirenderöity staattinen runko suoratoistettaviin dynaamisiin alueisiin PPR-mallilla. Harjoittelet Next.js 15 -fullstack-kehitys (App Router + Server Actions)-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Next.js 15 -fullstack-kehitys (App Router + Server Actions)-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunnilla?

Kyllä. Jokainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Suspense-rajat ja komponenttitason suoratoisto
  2. Merkityksellisten loading.tsx-tiedostojen ja skeletonien suunnittelu
  3. Osittainen esirenderöinti: staattinen runko ja dynaamiset aukot
  4. Suoratoiston sudenkuopat: asettelun siirtyminen ja vesiputoukset
← Takaisin: Next.js 15 -fullstack-kehitys (App Router + Server Actions)