Przejdź do treści

Jak działa Partial Prerendering (PPR) w Next.js?

Zapomnij o wyborze między statyczną stroną a dynamicznym SSR. Zobacz, jak PPR łączy prerenderowaną powłokę z dynamicznym generowaniem treści w locie.

Maciej Sala

Founder StriveLab

9 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 17 lipca 2026)

Czym jest Partial Prerendering w Next.js?

Partial Prerendering (PPR) to model renderingu w Next.js, który pozwala na jednej stronie łączyć elementy statyczne (generowane w build time) z elementami dynamicznymi (renderowanymi na żądanie).

Jak pisałem we wstępie, deweloperzy musieli wybierać, czy cała strona jest statyczna (/) czy cała ma być dynamiczna (). Dzięki PPR możemy pogodzić te dwie rzeczy, ponieważ prerenderowana powłoka strony jest wysyłana bez oczekiwania na dynamiczne „wyspy", które w tle. Dostarczenie powłoki bezpośrednio z jest optymalizacją infrastruktury.

W starszych wersjach Next.js PPR było pokazywane jako osobna, eksperymentalna funkcja, ale w aktualnych wersjach ten sam kierunek rozwija się pod nazwą Cache Components i opiera się na cacheComponents, dyrektywie "use cache" oraz Suspense.

Innymi słowy, zmienia się poziom decyzji, ponieważ teraz nie wybieramy już renderingu dla całej strony. Wybieramy go dla konkretnych komponentów.

Jak PPR działa pod maską w Next.js?

PPR opiera się na trzech mechanizmach:

  1. Statyczna powłoka, czyli layout, nagłówek, nawigacja, footer, fallbacki i treści, które można ukończyć podczas prerenderowania. Powłoka może być obsłużona przez origin albo, przy odpowiedniej integracji platformy, z CDN.

  2. Dynamiczne wyspy (holes). Fragmenty pod <Suspense>, które wymagają danych zależnych od żądania albo niecache'owanego I/O. Samo <Suspense> nie czyni synchronicznego komponentu dynamicznym.

  3. Streaming. Dynamiczne wyspy są renderowane na serwerze przy każdym żądaniu i streamowane do przeglądarki, wypełniając „dziury" w statycznej powłoce.

Code
Request → hosting wysyła gotową powłokę z fallbackami
       → Przeglądarka renderuje powłokę ze skeletonami
       → Serwer renderuje dynamiczne wyspy
       → Streaming wypełnia skeletony prawdziwą treścią

Jak włączyć Partial Prerendering w Next.js?

W aktualnym podejściu włączasz ten model przez Cache Components:

Code
// next.config.ts
const nextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

W starszych materiałach, zobaczysz jeszcze experimental.ppr = 'incremental' i export const experimental_ppr = true. Aktualnie, praktycznym punktem odniesienia są cacheComponents, "use cache" i Suspense.

Oznaczanie strony jako PPR

Code
// app/product/[id]/page.tsx
 
import { Suspense } from 'react'
 
export function generateStaticParams() {
  return [{ id: 'example-product' }]
}
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
 
  return (
    <main>
      {/* STATYCZNE — generowane w build time */}
      <Header />
      <Breadcrumbs product={id} />
 
      {/* STATYCZNE — dane produktu z cache */}
      <ProductInfo id={id} />
 
      {/* DYNAMICZNE — zależne od sesji użytkownika */}
      <Suspense fallback={<PriceSkeleton />}>
        <PersonalizedPrice id={id} />
      </Suspense>
 
      {/* DYNAMICZNE — zależne od cookies */}
      <Suspense fallback={<CartButtonSkeleton />}>
        <AddToCartButton id={id} />
      </Suspense>
 
      {/* STATYCZNE — opis, specyfikacja */}
      <ProductDescription id={id} />
 
      {/* DYNAMICZNE — real-time dane */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <LiveReviews id={id} />
      </Suspense>
 
      {/* STATYCZNE */}
      <Footer />
    </main>
  )
}

W tym modelu nie oznaczasz już całej strony pojedynczą flagą dla danej ścieżki, ale buforuj wybrane fragmenty za pomocą dyrektywy "use cache", podczas gdy operacje oraz dane zależne od czasu żądania umieszczaj wewnątrz komponentów <Suspense>. Oznaczenia takie jak „statyczne” w przykładowym kodzie zakładają, że dany element wykonuje wyłącznie zadania możliwe do zrealizowania podczas wstępnego prerenderowania lub sam korzysta z buforowania. Z kolei funkcja generateStaticParams() dostarcza Next.js przykładowe parametry niezbędne do wygenerowania powłoki trasy dynamicznej.

Co sprawia, że komponent w PPR jest dynamiczny?

Next.js automatycznie wykrywa, które komponenty są dynamiczne na podstawie użycia API zależnych od requestu:

Code
// DYNAMICZNY — czyta cookies
import { cookies } from 'next/headers'
 
async function PersonalizedPrice({ id }: { id: string }) {
  const cookieStore = await cookies()
  const userSegment = cookieStore.get('segment')?.value || 'standard'
 
  const price = await fetch(
    `https://api.example.com/pricing/${id}?segment=${userSegment}`,
    { cache: 'no-store' },
  )
  const data = await price.json()
 
  return <span className="text-2xl font-bold">{data.price} PLN</span>
}
Code
// DYNAMICZNY — czyta headers
import { headers } from 'next/headers'
 
async function GeoContent() {
  const headersList = await headers()
  const country = headersList.get('x-country') || 'PL'
  // ...
}
Code
// STATYCZNY — nie używa cookies/headers, dane cachowane
import { cacheLife, cacheTag } from 'next/cache'
 
async function ProductInfo({ id }: { id: string }) {
  'use cache'
  cacheLife('hours')
  cacheTag(`product-${id}`)
 
  const product = await fetch(`https://api.example.com/products/${id}`)
  if (!product.ok) throw new Error('Nie udało się pobrać produktu')
  const data = await product.json()
 
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
    </div>
  )
}

Przy włączonych Cache Components zwykłe asynchroniczne I/O nie trafia automatycznie do prerenderowanej powłoki (fetch, zapytanie ORM czy odczyt pliku). W sytuacji gdy dane mają być świeże przy każdym żądaniu, umieść pracę pod <Suspense>, a jak mają być współdzielone i okresowo odświeżane, użyj "use cache" z cacheLife i ewentualnie cacheTag. Żądania API, takie jak odczyt cookies() czy headers(), wykonuj poza zakresem objętym buforowaniem, a wybrane wartości przekaż do funkcji cachowanej jako argumenty, aby świadomie kontrolować strukturę klucza cache.

Rewalidacja cache'owanych fragmentów

cacheLife('hours') określa profil czasowy, a cacheTag z tagiem konkretnego produktu pozwala unieważnić jego wpis po zmianie. W Server Action użyj updateTag(tag), jeśli użytkownik powinien natychmiast zobaczyć własny zapis. W webhooku lub innym scenariuszu akceptującym stale-while-revalidate użyj revalidateTag(tag, 'max'). Bez strategii rewalidacji szybka powłoka może wyświetlać nieaktualną cenę lub dostępność.

Unikaj przekazywania tokenów sesji oraz innych poufnych danych jako argumentów do standardowej dyrektywy "use cache", ponieważ choć argumenty współtworzą klucz buforowania, sam wynik staje się ogólnodostępnym zasobem. W przypadku informacji o charakterze ściśle prywatnym najlepiej zachować pełną dynamikę renderowania. Rozwiązanie typu "use cache: private" pozwala co prawda na odczyt zapytań w czasie żądania, jednak wciąż pozostaje funkcją eksperymentalną, której nie zaleca się w środowisku produkcyjnym (zapisuje ona wynik wyłącznie w pamięci podręcznej przeglądarki, przez co dane znikają już po odświeżeniu strony).

PPR w praktyce na stronie e-commerce

Najlepszym przykładem wartości PPR jest strona produktowa w sklepie internetowym.

Bez PPR: cała strona może czekać na zakończenie procesu SSR, jeśli spersonalizowana cena oraz stan koszyka są odczytywane przed wyrenderowaniem odpowiedzi. Rzeczywisty wskaźnik TTFB zależy bezpośrednio od hostingu, regionu oraz wydajności źródeł danych, dlatego też nie istnieje jeden uniwersalny zakres czasowy dla renderowania serwerowego.

Z PPR: wstępna powłoka obejmująca układ strony, opis produktu, specyfikację oraz odpowiednie elementy zastępcze może zostać wysłana do klienta jeszcze przed obliczeniem ceny i zawartości koszyka. To, czy strona serwowana jest bezpośrednio z CDN i jak szybko dociera do użytkownika, pozostaje uzależnione od specyfiki wdrożenia.

Code
import { cacheLife, cacheTag } from 'next/cache'
import { Suspense } from 'react'
 
async function getProduct(id: string) {
  'use cache'
  cacheLife('hours')
  cacheTag(`product-${id}`)
 
  const response = await fetch(`https://api.example.com/products/${id}`)
  if (!response.ok) throw new Error('Nie udało się pobrać produktu')
  return response.json()
}
 
export async function generateStaticParams() {
  return [{ id: 'nike-air-max-90' }, { id: 'adidas-superstar' }]
}
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const product = await getProduct(id) // Cachowane — statyczne
 
  return (
    <main className="mx-auto max-w-4xl">
      {/* === PRERENDEROWANA POWŁOKA === */}
      <nav>
        <Breadcrumbs
          items={[
            { label: 'Sklep', href: '/sklep' },
            { label: product.category, href: `/sklep/${product.categorySlug}` },
            { label: product.name },
          ]}
        />
      </nav>
 
      <div className="grid grid-cols-2 gap-8">
        <ProductGallery images={product.images} />
 
        <div>
          <h1 className="text-3xl font-bold">{product.name}</h1>
          <p className="mt-2 text-gray-600">{product.shortDescription}</p>
 
          {/* === DYNAMICZNA WYSPA: spersonalizowana cena === */}
          <Suspense
            fallback={
              <div className="mt-4 h-10 w-32 animate-pulse rounded bg-gray-200" />
            }
          >
            <DynamicPricing productId={id} />
          </Suspense>
 
          {/* === DYNAMICZNA WYSPA: koszyk === */}
          <Suspense
            fallback={
              <div className="mt-4 h-12 w-full animate-pulse rounded bg-gray-200" />
            }
          >
            <AddToCart productId={id} />
          </Suspense>
 
          {/* === STATYCZNE: specyfikacja === */}
          <ProductSpecs specs={product.specifications} />
        </div>
      </div>
 
      {/* === STATYCZNE: opis === */}
      <section className="prose mt-12 max-w-none">
        <h2>Opis produktu</h2>
        <div dangerouslySetInnerHTML={{ __html: product.fullDescription }} />
      </section>
 
      {/* === DYNAMICZNA WYSPA: opinie === */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={id} />
      </Suspense>
 
      {/* === STATYCZNE: powiązane produkty === */}
      <RelatedProducts categoryId={product.categoryId} />
    </main>
  )
}

Właściwość dangerouslySetInnerHTML jest bezpieczna wyłącznie wtedy, gdy renderowany kod HTML został wcześniej dokładnie oczyszczony po stronie serwera lub pochodzi z bezwzględnie zaufanego źródła. Nigdy nie używaj tego rozwiązania do wyświetlania surowej treści od użytkowników ani niezweryfikowanych odpowiedzi z systemów CMS. Z kolei komponent ProductReviews stanie się elementem zależnym od czasu żądania tylko wtedy, gdy faktycznie wykona operację bez buforowania lub skorzysta z danych żądania, ponieważ sama obecność granicy <Suspense> nie wymusza takiego zachowania.

PPR a SEO i Core Web Vitals

PPR może wspierać SEO i wydajność z kilku powodów:

Szybszy TTFB dzięki statycznej powłoce

Prerenderowana powłoka może obniżyć TTFB i szybciej pokazać użyteczną część dokumentu, ale nie jest jedną z trzech Core Web Vital, a metryką diagnostyczną.Wpływ PPR trzeba zmierzyć na realnym wdrożeniu.

Krytyczna treść dostępna w powłoce

Crawler może zobaczyć prerenderowaną powłokę przed ukończeniem dynamicznych fragmentów, których odbiór zależy od czasu odpowiedzi i procesu renderowania. W związku z tym, krytyczne elementy SEO, takie jak H1, lead, opis produktu i główna treść, najlepiej utrzymywać w powłoce, a nie odkładać ich do późno streamowanych komponentów.

Lepszy LCP dzięki Partial Prerendering

Jeżeli kandydat LCP (np. hero albo zdjęcie produktu) znajduje się w powłoce i jego zasób jest poprawnie priorytetyzowany, może pojawić się bez czekania na dynamiczne dane. PPR może więc poprawić , ale nie gwarantuje dobrego wyniku, ponieważ nadal liczą się rozmiar obrazu, CSS, fonty, połączenie użytkownika i obciążenie głównego wątku.

Ograniczenia Partial Prerendering

Ewolucja API PPR od eksperymentu do stabilności

To obszar, który szybko się rozwijał, więc łatwo trafić na nieaktualne materiały. Wczesna flaga experimental.ppr i route config experimental_ppr zostały usunięte na rzecz Cache Components. W Next.js 16 cacheComponents jest top-level config, a "use cache", cacheLife i cacheTag są dostępne bez prefiksu unstable_. unstable_cache należy do poprzedniego modelu cache i nie jest tym samym API co "use cache"; przy migracji korzystaj z przewodnika dla Cache Components zamiast mechanicznie zamieniać nazwy.

PPR wymaga Suspense boundaries

Każdy fragment z niecache'owanym I/O lub danymi czasu żądania potrzebuje nadrzędnego <Suspense>. Jeśli go brakuje to cacheComponents: true Next.js zgłosi błąd Uncached data was accessed outside of <Suspense> podczas developmentu lub buildu i nie przełączy po cichu całej strony na SSR. Granicę umieszczaj możliwie blisko dynamicznego fragmentu, bo wszystko pod nią wypada z powłoki i czeka na request-time rendering.

Fallback <Suspense> nie obsługuje błędów, tylko stan oczekiwania, więc dla dynamicznej wyspy zaplanuj również Error Boundary, np. route-level error.tsx, i obserwowanie po stronie serwera. Jeśli wolny fragment zostanie zastąpiony przez szkielet ładowania (skeleton), odrzucona obietnica może doprowadzić do awarii całego procesu renderowania, zamiast wyświetlić kontrolowany komunikat o błędzie.

PPR wymaga platformy ze streamingiem

PPR działa na pojedynczym serwerze next start i w kontenerze Docker. Korzyść z progresywnego dostarczania wymaga streamingu end-to-end: load balancer, CDN i reverse proxy nie mogą buforować odpowiedzi. Bez tego wynik może pozostać funkcjonalnie poprawny, ale powłoka i dynamiczna treść dotrą razem. Przy wielu instancjach potrzebujesz również współdzielonego cache i koordynacji tagów, jeśli oczekujesz spójnej rewalidacji. output: 'export' nie obsługuje Cache Components.

Debugowanie Partial Prerendering

Ustalenie, które komponenty są statyczne, a które dynamiczne, może nie być takie oczywiste. Next.js dev mode pokazuje wskaźniki (Static Indicator), ale w złożonych drzewach komponentów zależności bywają trudne do wyśledzenia.

Code
# Ogólny status tras statycznych i dynamicznych
next build --debug
 
# Więcej informacji o błędach prerenderowania
next build --debug-prerender

Nie opieraj diagnostyki na dawnym symbolu , ponieważ aktualny output CLI rozróżnia przede wszystkim trasy statyczne () i dynamiczne (ƒ). Aby sprawdzić, co faktycznie weszło do powłoki PPR, obejrzyj źródło HTML odpowiedzi oraz fallbacki, a zachowanie streamingu przetestuj na docelowej infrastrukturze z wyłączonym buforowaniem proxy.

PPR vs SSR, SSG i ISR w Next.js

ModelGenerowaniePersonalizacjaCharakterystyka odpowiedziUżycie
SSGBuild timeBrak w HTMLGotowy dokument bez pracy renderera na żądanieBlogi, docs, landing pages
ISRPrerender + rewalidacjaBrak w cacheGotowy dokument okresowo odświeżanyProdukty, artykuły z CMS
SSRRenderowanie na każde żądaniePełnaTTFB zależy od całej pracy blokującej odpowiedźDashboardy, panele admin
PPRPowłoka + request-time fragmentsCzęściowaPowłoka może dotrzeć przed dynamicznymi fragmentamiE-commerce, strony z personalizacją

PPR nie zastępuje SSG ani ISR, ale je uzupełnia. Strona czysto statyczna (blog, dokumentacja) nie potrzebuje PPR. Idealnym kandydatem będzie strona, która jest w 80% statyczna, ale ma 20% dynamicznej personalizacji.

Jak przygotować projekt Next.js na PPR?

Nawet jeśli nie włączysz PPR dzisiaj, możesz przygotować architekturę:

  1. Oddzielaj komponenty statyczne od dynamicznych. Nie mieszaj cookies() i statycznych danych w jednym komponencie.
  2. Używaj Suspense celowo. Otaczaj nim niecache'owane I/O i dane zależne od żądania, a nie każdy komponent z danymi.
  3. Cache'uj świadomie. Współdzielone dane, dla których akceptujesz nieaktualność, oznaczaj "use cache" i dobieraj cacheLife oraz cacheTag.
  4. Przenoś dynamiczne API do dedykowanych komponentów. cookies() i headers() tylko w liściach drzewa komponentów.

Te praktyki ułatwiają wdrożenie Cache Components, ale nie zastępują pomiaru wydajności, strategii rewalidacji ani testu hostingu pod kątem streamingu.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czy PPR (Cache Components) wymaga Vercel?

Nie. Cache Components działa na serwerze Node.js i w kontenerze Docker; wsparcie adapterów zależy od platformy. Pełna korzyść ze streamingu wymaga, aby proxy i hosting nie buforowały odpowiedzi. Statyczny export (output: 'export') nie obsługuje tego modelu, bo nie ma serwera renderującego fragmenty zależne od żądania.

Czy mogę użyć tego modelu tylko na wybranych stronach?

cacheComponents włączasz globalnie dla App Routera, ale granice prerenderowania wyznaczasz w poszczególnych trasach i komponentach. Cache'owane fragmenty oznaczasz "use cache", a niecache'owane dane czasu żądania umieszczasz pod <Suspense>. Trasa bez takich fragmentów może być w pełni prerenderowana.

Czym PPR różni się od zwykłego streamingu z Suspense?

Zwykły streaming może renderować trasę na żądanie i wysyłać jej HTML w porcjach. PPR dodaje powłokę utworzoną podczas prerenderowania: fallbacki są gotowe wcześniej, a niecache'owane fragmenty renderują się na żądanie. Powłoka może być dostarczona z CDN tylko wtedy, gdy platforma zapewnia odpowiednią integrację; pojedynczy serwer Node również obsługuje PPR.

Czy ten model renderingu jest już stabilny?

W Next.js 16 PPR jest częścią opt-in Cache Components. cacheComponents jest konfiguracją najwyższego poziomu, a use cache, cacheLife i cacheTag są dostępne bez prefiksu unstable_. Dawne experimental.ppr i route config experimental_ppr zostały usunięte.

Co czyni komponent dynamicznym w tym modelu?

API zależne od żądania (cookies(), headers(), searchParams, czasem params) oraz niecache'owane operacje asynchroniczne, np. fetch lub zapytanie do bazy. Taki fragment umieść pod <Suspense> albo cache'uj go, jeśli nie korzysta z danych żądania. Bez jawnej decyzji Next.js zgłosi błąd podczas developmentu lub buildu.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Jak React Server Components wpływają na SEO i performance?

Przez lata React pchał, coraz więcej i więcej pracy do przeglądarki, w efekcie czego mamy większe bundle, hydratacja trwa dłużej, są puste loadingi i strony, które bez JavaScriptu straciły sens. App Router odwraca ten kierunek. React Server Components pozwalają renderować treść na serwerze bez wysyłania całej logiki do klienta, a Server Actions upraszczają formularze i mutacje. To ma znaczenie dla SEO, bo crawler szybciej dostaje HTML. Ma też znaczenie dla biznesu, bo użytkownik szybciej widzi i dostaje to, po co przyszedł.

Maciej Sala

Maciej Sala

Founder StriveLab

ISR na żądanie — jak odświeżać tylko te strony, które się zmieniły

Czysto hipotetycznie, projekt ma 2000 stron produktowych. Zmieniasz z jakiegoś powodu cenę na jednej i w klasycznym SSG czeka Cię pełna przebudowa całego serwisu. Tak niefektywne, że aż absordualne. ISR w Next.js rozwiązuje to: regenerujesz tylko tę jedną stronę w tle i na żądanie. Astro podchodzi do problemu inaczej: albo zostajesz przy statycznym budowaniu i webhookach, albo przechodzisz na renderowanie na żądanie, cache CDN i Server Islands. W tym artykule pokazuję, gdzie kończy się natywne ISR Next.js i jak rozwiązać ten sam problem w Astro.

Maciej Sala

Maciej Sala

Founder StriveLab

Server Islands w Astro — dynamiczne fragmenty na statycznej stronie

Przez lata architekci webowi stali przed fałszywym wyborem: albo pełna statyka i wolność od serwera, albo SSR i dynamika na żądanie. Server Islands to koniec tego dylematu. Jeden element wymaga danych w czasie rzeczywistym — reszta strony pozostaje statyczna i bezpieczna w CDN . Inżynieria punktowa, nie przebudowa całej strony.

Maciej Sala

Maciej Sala

Founder StriveLab