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:
-
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.
-
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. -
Streaming. Dynamiczne wyspy są renderowane na serwerze przy każdym żądaniu i streamowane do przeglądarki, wypełniając „dziury" w statycznej powłoce.
Jak włączyć Partial Prerendering w Next.js?
W aktualnym podejściu włączasz ten model przez Cache Components:
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
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:
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.
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.
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
| Model | Generowanie | Personalizacja | Charakterystyka odpowiedzi | Użycie |
|---|---|---|---|---|
| SSG | Build time | Brak w HTML | Gotowy dokument bez pracy renderera na żądanie | Blogi, docs, landing pages |
| ISR | Prerender + rewalidacja | Brak w cache | Gotowy dokument okresowo odświeżany | Produkty, artykuły z CMS |
| SSR | Renderowanie na każde żądanie | Pełna | TTFB zależy od całej pracy blokującej odpowiedź | Dashboardy, panele admin |
| PPR | Powłoka + request-time fragments | Częściowa | Powłoka może dotrzeć przed dynamicznymi fragmentami | E-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ę:
- Oddzielaj komponenty statyczne od dynamicznych. Nie mieszaj
cookies()i statycznych danych w jednym komponencie. - Używaj Suspense celowo. Otaczaj nim niecache'owane I/O i dane zależne od żądania, a nie każdy komponent z danymi.
- Cache'uj świadomie. Współdzielone dane, dla których akceptujesz nieaktualność, oznaczaj
"use cache"i dobierajcacheLifeorazcacheTag. - Przenoś dynamiczne API do dedykowanych komponentów.
cookies()iheaders()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.
