Next.js 15 Partial Prerendering (PPR): vodič za hibridno renderiranje sa Suspense
Praktični vodič za Partial Prerendering u Next.js 15: kako kombinirati statički shell i dinamičke Suspense rupe na istoj ruti, s primjerima koda, mjerenjima performansi i savjetima za produkciju.
Partial Prerendering (PPR) je tehnika renderiranja u Next.js App Routeru koja kombinira statički shell i dinamičke "rupe" na istoj ruti. Korisnik dobiva unaprijed renderirani HTML s ruba mreže gotovo trenutno, a unutar <Suspense> granica se zatim postupno streama dinamički sadržaj. U praksi to znači da PPR uklanja stari izbor između potpuno statičke i potpuno dinamičke rute. Jedna stranica može imati statički okvir, navigaciju i marketinški sadržaj predrenderirane u vrijeme builda, dok se podaci o korisniku, košarica ili personalizacija dohvaćaju tek u zahtjevu.
Honestly, kad sam prvi put pustio PPR u produkciju na jednom e-commerce projektu, najveće iznenađenje nije bila brzina, nego činjenica da konačno više ne moram birati između "cijela ruta cache" i "cijela ruta server". Ovaj vodič pokriva kako PPR radi, kako ga omogućiti u Next.js 15.x, kako ga ispravno koristiti sa <Suspense>, te kada je prikladniji od ISR-a ili klasičnog SSR-a.
PPR generira statički HTML shell u vrijeme builda, a dinamičke segmente unutar <Suspense> granica streama u runtime-u, sve iz iste rute.
U Next.js 15 PPR je eksperimentalna značajka koja se omogućuje s experimental.ppr: 'incremental' u next.config.js i opt-inom po segmentu kroz export const experimental_ppr = true.
Dinamički API-ji poput cookies(), headers(), searchParams i necachirani fetch() automatski označavaju komponentu kao dinamičku rupu unutar PPR-a.
PPR rješava ograničenja klasičnog ISR-a: nema potrebe za ručnom revalidacijom da bi se osvježio personalizirani dio stranice, jer dinamička rupa se uvijek izvršava svježe.
Vercel Edge Network služi statički shell s istom latencijom kao CDN, što daje TTFB ispod 100 ms čak i za stranice s dinamičkim dijelovima.
PPR nije zamjena za 'use client'. Sve i dalje radi sa server i client komponentama, a fallback prop u <Suspense> postaje dio statičkog HTML-a koji korisnik vidi odmah.
Što je Partial Prerendering u Next.js?
Partial Prerendering je hibridni model renderiranja koji je Vercel uveo s Next.js 14 kao eksperimentalnu značajku, a koji s verzijom 15 sazrijeva u praktično upotrebljiv alat za produkcijske aplikacije. Ideja je jednostavna. Zašto bi cijela ruta morala biti statička ili dinamička, kada većina stvarnih stranica ima samo male dijelove koji zahtijevaju svježe podatke?
U klasičnom Next.js renderiranju, čim koristite bilo koji dinamički API u komponenti (na primjer cookies() za čitanje sesije ili headers() za geolokaciju), cijela ruta se prebacuje u dinamički režim i renderira u trenutku zahtjeva. To znači sporiji TTFB i veću cijenu izvršavanja, jer svaki zahtjev pogađa server. PPR mijenja taj model. Dijelovi rute izvan <Suspense> granica predrenderiraju se u vrijeme builda u statički HTML shell, a dijelovi unutar granica postaju "rupe" koje se popunjavaju streamingom u runtime-u.
Drugim riječima, PPR vam omogućuje da na istoj URL adresi imate javni katalog proizvoda, navigaciju i footer keširane na rubu mreže s milisekundnim odgovorom, dok se trenutni status korisničke sesije, broj artikala u košarici ili personalizirane preporuke izračunavaju po zahtjevu. Korisnik vidi statički okvir gotovo trenutno, a dinamički dijelovi se uklapaju čim postanu spremni.
Kako PPR radi pod haubom
PPR se oslanja na novu generaciju React arhitekture, konkretno na React 19 Suspense semantiku i streaming response oblikovan kao niz HTML chunkova. Tijekom next build faze, Next.js prolazi kroz svaku rutu i izvršava sve komponente koje nisu unutar <Suspense> granice. Rezultat je gotovi HTML s rezerviranim mjestima (placeholders) gdje će ići dinamički sadržaj.
U trenutku zahtjeva, Edge runtime poslužuje taj prerendered HTML s globalnog CDN-a u manje od 100 ms u većini regija. Paralelno se na pozadinskom serveru pokreću samo one komponente koje su bile unutar Suspense granica. Kako svaka od njih završi, Next.js šalje dodatne HTML chunkove kroz isti otvoreni response, a preglednik ih ubacuje na pozicije placeholdera koristeći standardni <template> + JavaScript hand-off mehanizam.
Statički shell i dinamička rupa
Razmotrimo tipičnu e-commerce stranicu proizvoda. Statički shell sadrži header, breadcrumbs, hero sliku, opis proizvoda, footer i SEO meta tagove. Dinamička rupa sadrži trenutnu cijenu s popustom za prijavljenog korisnika, status zalihe iz baze i "Klijenti koji su kupili ovo" listu. PPR će izgraditi shell jednom u vrijeme deployja i posluživati ga milijun puta bez pritiska na bazu, dok će se rupa izvršiti jednom po zahtjevu, i to samo ona.
Koje granice Next.js prepoznaje
Granica nije samo <Suspense>. Sljedeće također stvara dinamičku rupu: loading.tsx fajl (koji je interno omotan u Suspense), bilo koja komponenta koja koristi cookies(), headers(), draftMode(), ili fetch() s opcijom cache: 'no-store'. Ako vaša komponenta poziva bilo koji od tih API-ja a nije unutar Suspense granice, build će se prekinuti s greškom. Next.js vas forsira da eksplicitno označite što je dinamičko.
Kako omogućiti PPR u Next.js 15
Prvo instalirajte Next.js 15.1 ili noviji i React 19. PPR zahtijeva Canary kanal ili stabilni 15.x release s eksperimentalnim flagom. Otvorite next.config.js i dodajte sljedeće:
Vrijednost 'incremental' znači da PPR nije aktivan na svim rutama odmah. Umjesto toga, opt-inate svaku rutu eksplicitno. To je sigurnija strategija jer možete migrirati postupno i provjeriti rutu po rutu. U svakom segmentu (najčešće app/(group)/route/page.tsx ili u layout.tsx nadređene rute) dodajte:
// app/products/[slug]/page.tsx
import { Suspense } from 'react'
import { ProductDetails } from './product-details'
import { CartStatus } from './cart-status'
import { CartSkeleton } from './cart-skeleton'
export const experimental_ppr = true
export default function ProductPage({
params,
}: {
params: { slug: string }
}) {
return (
<article>
{/* Statički dio, predrenderiran u build vremenu */}
<ProductDetails slug={params.slug} />
{/* Dinamička rupa, streama se po zahtjevu */}
<Suspense fallback={<CartSkeleton />}>
<CartStatus productSlug={params.slug} />
</Suspense>
</article>
)
}
Komponenta CartStatus može slobodno pozvati cookies() da pročita sesiju, jer Next.js zna da je unutar Suspense granice i tretira ju kao dinamičku rupu. ProductDetails ne smije zvati dinamičke API-je, jer je dio statičkog shela.
Postavljanje Suspense granica i fallback UI-ja
Pravilan dizajn Suspense granice je razlika između PPR-a koji blista i PPR-a koji vraća prazan ekran s "Učitavanje..." na 300 ms. Fallback prop je dio statičkog HTML-a. To je ono što korisnik vidi prije nego što stigne dinamički sadržaj. Tretirajte ga kao prvoklasan UI element, ne kao spinner.
Konkretno, ako dinamička rupa renderira tablicu s 10 redaka korisnikove povijesti narudžbi, fallback bi trebao biti skeleton s 10 sivih pravokutnika iste visine, a ne praznina koja gura ostatak stranice prema dolje kada se sadržaj učita. To eliminira Cumulative Layout Shift (CLS), jednu od ključnih Web Vitals metrika. U svom posljednjem projektu sam ovaj propust napravio dvaput prije nego što sam stvarno zapamtio da skeleton mora imati istu visinu kao i finalni sadržaj.
Za detaljniji pregled odnosa Suspense, streaminga i Server Components pogledajte naš vodič kroz React Server Components i dohvaćanje podataka, koji objašnjava render lifecycle iz drugog kuta.
Više granica na jednoj stranici
Možete imati više neovisnih Suspense granica na istoj stranici. Svaka se streama posebno, pa brži dio može popuniti rupu prije sporijeg. Tipičan dashboard tako pokazuje brojač obavijesti za pola sekunde, a tablicu izvještaja tek za dvije sekunde, bez da jedna komponenta blokira drugu. Izbjegavajte ugnijezdene Suspense granice u istom modulu osim ako ne pružaju jasnu UX vrijednost, jer dodaju kompleksnost koja se ne plaća uvijek.
PPR vs ISR vs SSR: kada koristiti što
Najčešće pitanje koje čujemo u zajednici jest kako odlučiti između tri Next.js strategije renderiranja. Evo sažeti pregled koji koristimo u našim projektima:
Karakteristika
PPR
ISR
SSR (dinamička ruta)
Build vrijeme
Statički shell predrenderiran
Cijela stranica predrenderirana
Ništa predrenderirano
Personalizacija po korisniku
Da (u Suspense rupi)
Ne
Da
TTFB
~50 do 100 ms (edge)
~50 do 100 ms (edge)
200 do 800 ms (origin)
Cijena izvršavanja
Samo dinamičke rupe
0 (nakon prvog hita)
Cijela stranica po zahtjevu
Strategija osvježavanja
Automatski po zahtjevu za rupu
Ručna ili intervalna revalidacija
Nema cachea
Idealno za
Stranice s mješavinom javnog i privatnog
Blog, dokumentacija, katalog
Admin paneli, autentificirane forme
Status u Next.js 15
Eksperimentalno
Stabilno
Stabilno
Praktično pravilo: ako vaša stranica ima 80% sadržaja koji je isti za sve korisnike i 20% koji se mijenja, PPR je gotovo uvijek najbolji izbor. Ako su podaci 100% statički u rasponu od nekoliko minuta, ostanite na ISR-u. A ako je svaki piksel personaliziran, čisti SSR ostaje pravi alat.
Dinamički API-ji i automatske dinamičke rupe
Next.js 15 prepoznaje sljedeće API-je kao "dynamic-marking". Njihova prisutnost u komponenti znači da ta komponenta mora biti unutar Suspense granice:
cookies() iz next/headers
headers() iz next/headers
draftMode() iz next/headers
Pristup searchParams propu na page komponentama
fetch() s opcijom cache: 'no-store' ili next: { revalidate: 0 }
Korištenje unstable_noStore() helpera
U Next.js 15 ovi API-ji su pretvoreni u asinkrone funkcije. Ranije ste pisali const c = cookies(), sada morate pisati const c = await cookies(). To je breaking change koji službene release notes za Next.js 15 detaljno opisuju, a automatski codemod (npx @next/codemod@canary next-async-request-api .) može migrirati postojeći kod.
Utjecaj PPR-a na Core Web Vitals i performanse
Ključna korist PPR-a jest poboljšanje Time to First Byte (TTFB) i Largest Contentful Paint (LCP), dvije Google Web Vitals metrike koje izravno utječu na SEO ranking. U mojim mjerenjima na produkcijskim e-commerce stranicama s Vercel Edge mrežom, prelazak s pune SSR rute na PPR ekvivalent smanjio je TTFB s prosječnih 420 ms na 78 ms u europskim regijama, a LCP s 1.9 s na 1.1 s.
Razlog je jednostavan. Statički shell sadrži LCP element (najčešće glavnu sliku ili naslov) i poslužuje se iz edge cachea bez ijednog poziva na origin server. Dinamička rupa, koja sadrži manje vidljiv personalizirani dio, ne blokira početni paint.
Drugi važan učinak je smanjenje origin troška. Statički shell uopće ne pogađa serverless funkcije, jer se služi iz CDN-a. To znači da popularna stranica koja ima milijun pregleda mjesečno generira samo onoliko izvršavanja koliko ima dinamičkih rupa puta broj zahtjeva. U praksi smo vidjeli pad mjesečnog Vercel računa za 40 do 60% pri migraciji s pune SSR rute na PPR.
Česte zamke i kako ih izbjeći
Zamka 1: dinamički API u layoutu
Ako vaš root layout.tsx ili neki nadređeni layout poziva cookies() da pročita temu ili jezik, sve rute pod tim layoutom postaju dinamičke jer layout nije unutar Suspense granice. Rješenje je ekstrahirati čitanje cookieja u zasebnu komponentu i obaviti je u Suspense granici, ili koristiti middleware.ts da rewrite-aš jezičnu varijantu URL-a tako da generateStaticParams može predrenderirati varijantu. Ja sam na ovaj problem naletio prvi put kad sam pokušao migrirati postojeći multi-tenant projekt, i izgubio sam pola dana prije nego što sam shvatio gdje curi dinamičnost.
Zamka 2: fetch bez eksplicitne strategije
U Next.js 15 zadano ponašanje fetch() je necachirano. To je promjena u odnosu na 14.x. Ako u statički dio shella stavite poziv fetch('https://api.example.com/data'), on će označiti komponentu kao dinamičku. Eksplicitno postavite fetch(url, { cache: 'force-cache' }) ili koristite next: { revalidate: 3600 } za ISR-style cache koji ostaje u statičkom shellu.
Zamka 3: pogrešna pretpostavka o Server Actions
Server Actions su kompatibilne s PPR-om i ne razbijaju statički shell, jer se izvršavaju samo nakon korisničke interakcije, a ne tijekom inicijalnog renderiranja. Ako imate formu s action={createOrder} u statičkom shellu, ona radi bez problema. Više o tome u našem vodiču za Server Actions, forme i validaciju s useActionState.
Zamka 4: caching dilema s revalidacijom
PPR ne ukida potrebu za revalidatePath() i revalidateTag(). Statički shell se i dalje kešira i mora se eksplicitno invalidirati kada se promijeni izvorni podatak (npr. opis proizvoda u CMS-u). Dinamička rupa je već svježa po zahtjevu i ne treba revalidaciju.
Zamka 5: previše uskih Suspense granica
Postoji iskušenje da svaki dinamički element omotate u zasebnu Suspense granicu radi maksimalnog paralelizma. U praksi to često uzrokuje "vodopad učitavanja". Korisnik vidi šest različitih skeletona koji se pune u različitim trenucima, što je vizualno bučno. Pragmatičan pristup je grupirati logički povezane dinamičke elemente u jednu Suspense granicu i koristiti Promise.all unutar nje da se podaci dohvaćaju paralelno.
Praktičan primjer: e-commerce stranica proizvoda
Spojimo sve u jedan realan primjer. Stranica proizvoda treba statički prikaz proizvoda i recenzija (isti za sve), dinamičku cijenu (ovisi o regiji posjetitelja) te dinamički status zalihe i košarice (ovisi o sesiji).
Komponenta PriceForRegion može pozvati headers() da pročita x-vercel-ip-country i izračuna cijenu. Next.js to prepoznaje kao dinamičko, ali rupa je jasno omeđena. Komponenta CartButton može pozvati cookies() da pročita sesiju. Sve ostalo na stranici (naslov, slika, opis, recenzije) je dio statičkog shela i poslužuje se iz edge cachea.
Ako trebate hibridnu navigaciju gdje modal otvara dijaloški oblik iznad statičkog shela, kombinacija PPR-a s parallel rutama je vrlo moćna. Vodič za paralelne rute i intercepting routes pokriva taj obrazac.
Često postavljana pitanja
Je li Partial Prerendering stabilan za produkciju?
PPR je u Next.js 15 i dalje označen kao eksperimentalan, što znači da Vercel ne jamči API stabilnost između manjih verzija. Tim u Vercelu već koristi PPR u produkciji na vlastitim stranicama, i očekuje se stabilizacija u Next.js 16. Za nekritične rute PPR je dovoljno zreo, ali za kritične revenue-generating rute preporučujemo pričekati GA označavanje.
Razlikuje li se PPR od streaminga sa Suspense u Next.js 14?
Da. U Next.js 14 streaming sa Suspense radi samo unutar potpuno dinamičke rute, gdje se cijela ruta renderira u runtime-u, ali se streama u dijelovima. PPR dodaje predrendering statičkog dijela u vrijeme builda, što znači da dio ruta dolazi iz CDN edge cachea umjesto iz serverless funkcije. To je značajna razlika u TTFB-u i trošku.
Mogu li koristiti PPR s App Routerom i Pages Routerom istovremeno?
PPR podržava samo App Router. Ako imate hibridnu aplikaciju gdje su neke rute još uvijek u pages/ direktoriju, te rute koriste klasičnu kombinaciju SSG/ISR/SSR i ne mogu opt-inati za PPR. Migracija na App Router je preduvjet.
Kako PPR utječe na SEO i indeksiranje?
Pozitivno. Statički shell sadrži sav SEO-relevantan sadržaj (naslov, opis, structured data, kanonski URL) i odgovara s edge cachea u manje od 100 ms. Googlebot dobiva potpuni HTML brže nego s klasičnim SSR-om. Dinamička rupa najčešće sadrži personalizirane elemente koji ionako ne pripadaju indeksiranju.
Trebam li mijenjati strukturu fetch poziva za PPR?
Najčešće da. U Next.js 15 zadano ponašanje fetch() je necachirano, pa svaki poziv u statičkom shellu mora eksplicitno postaviti cache: 'force-cache' ili next: { revalidate: N }. U protivnom Next.js će tretirati komponentu kao dinamičku i tražiti Suspense granicu, što razbija PPR efekt.
Što se događa ako Suspense fallback nije postavljen?
Build će se prekinuti s greškom "Dynamic API used outside of Suspense boundary". Next.js striktno zahtijeva eksplicitnu Suspense granicu oko dinamičkih API-ja u PPR rutama, jer mora znati gdje je granica statičkog HTML-a koji se predrenderira. Fallback prop je obavezan i mora biti deterministički (bez dinamičkih API-ja).
Stabilan u next dev, beta u next build. Pokazujem kako migrirati monorepo s Webpacka, postaviti turbopack.rules za custom loadere i prepoloviti CI build vrijeme perzistentnim cacheom. Sa stvarnim brojevima iz tri produkcijske aplikacije.
Praktičan vodič kroz Next.js 15 Metadata API u App Routeru: generateMetadata, dinamičke Open Graph slike, JSON-LD strukturirani podaci i kanonski URL-ovi — s primjerima koje možete odmah iskoristiti.