Sebagian besar perdebatan rendering di Next.js berujung pada pilihan biner: render seluruh route saat build demi kecepatan, atau render per permintaan demi kesegaran data. Partial Prerendering, atau PPR, menolak pilihan biner itu. PPR membiarkan satu route menyajikan shell statis secara instan, sementara bagian yang benar-benar membutuhkan data waktu permintaan baru di-streaming belakangan, dalam respons yang sama.
Tulisan ini membahas model mental di balik PPR, bagaimana batas Suspense menentukan bagian mana yang di-prerender versus di-streaming, contoh kerja yang bisa langsung disesuaikan, siklus hidup permintaan di baliknya, jebakan umum yang sering dialami tim, serta trade-off yang perlu dipahami sebelum mengaktifkannya pada sebuah route.
Model mental: shell dulu, lubang belakangan
Bayangkan sebuah halaman dashboard. Navigasi, judul halaman, dan teks pemasaran statis tidak pernah berubah antar permintaan. Namun feed pesanan yang berjalan langsung atau saldo akun pengguna bergantung pada siapa yang meminta dan kapan. Static rendering tradisional memaksa seluruh halaman menjadi sepenuhnya statis atau sepenuhnya dinamis, yang berarti satu widget personal saja bisa diam-diam menyeret seluruh route keluar dari cache statis dan masuk ke rendering dinamis penuh di setiap permintaan. PPR membagi perbedaan itu pada level komponen, bukan pada level route.
Saat build, Next.js menyusuri pohon komponen sebuah route dan mem-prerender semua yang bisa. Setiap kali menemukan komponen yang dibungkus batas Suspense, ia merender UI fallback sebagai gantinya dan meninggalkan sebuah lubang. Hasilnya adalah satu shell HTML, di-cache dan disajikan secara instan, dengan celah bernama tempat konten dinamis nanti disisipkan oleh runtime streaming milik framework itu sendiri. Karena shell tersebut adalah artefak yang di-cache seperti biasa, ia mendapat manfaat dari caching CDN dan edge yang sama seperti halaman statis mana pun, walaupun sebagian dari respons akhirnya bergantung pada pengunjung tertentu.
- Shell statis — layout, header, dan komponen apa pun yang tidak membaca data waktu permintaan, dibuat sekali dan di-cache seperti halaman statis biasa.
- Lubang dinamis — komponen yang dibungkus Suspense yang menggunakan API bercakupan permintaan, dirender saat permintaan tiba dan di-streaming ke slot yang sudah ditentukan.
- Satu respons — shell dan lubang yang di-streaming dikirim lewat satu permintaan HTTP, sehingga tidak ada round trip tambahan untuk bagian dinamisnya.
Membungkus komponen dengan Suspense saja tidak membuatnya menjadi dinamis. Sebuah komponen baru menjadi dinamis ketika ia membaca API waktu permintaan seperti cookies, headers, atau searchParams. Suspense adalah batas yang mengurung perilaku dinamis itu agar bagian lain halaman tetap bisa statis.
Mengaktifkannya: konfigurasi dan opt-in per route
PPR bersifat opt-in di dua tingkat. Pertama, file next.config mengatur opsi ppr menjadi string incremental, yang membuka adopsi per route alih-alih memaksakannya ke seluruh proyek. Kedua, setiap route yang ingin memakai PPR mengekspor konstanta experimental_ppr bernilai true dari file layout atau page-nya. Route tanpa ekspor tersebut secara default bernilai false dan dirender seperti biasa.
Flag experimental_ppr berlaku turun ke setiap layout dan page bersarang di bawah segmen route tersebut, sehingga hanya perlu diatur sekali di bagian atas segmen yang diadopsi. Segmen anak bisa keluar lagi dengan mengekspor experimental_ppr sebagai false, yang berguna ketika sebuah halaman bersarang tertentu benar-benar dinamis sepenuhnya dan tidak mendapat manfaat dari shell statis.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
ppr: 'incremental',
},
}
export default nextConfig
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { StaticHeader } from './static-header'
import { LiveOrderFeed, FeedSkeleton } from './live-order-feed'
export const experimental_ppr = true
export default function DashboardPage() {
return (
<>
<StaticHeader />
<Suspense fallback={<FeedSkeleton />}>
<LiveOrderFeed />
</Suspense>
</>
)
}Bagaimana sebuah permintaan sebenarnya mengalir
Setelah sebuah route mengaktifkan PPR dan memiliki setidaknya satu batas Suspense di sekitar konten dinamis, permintaan ke route tersebut mengikuti urutan yang bisa diprediksi. Memahami urutan ini penting karena menjelaskan mengapa PPR bisa terasa cepat walaupun bagian dinamis halaman sedang melakukan pekerjaan nyata, seperti kueri basis data atau fetch yang terautentikasi.
- Server langsung mengembalikan shell statis yang sudah di-prerender, termasuk UI fallback Suspense untuk setiap lubang dinamis, sehingga browser bisa menampilkan sesuatu yang bermakna seketika.
- Secara paralel, server mulai merender setiap komponen dinamis yang bergantung pada data waktu permintaan, seperti sesi berbasis cookie atau pembacaan basis data yang khusus untuk pengguna saat ini.
- Setiap bagian dinamis yang selesai akan di-streaming ke dalam lubangnya lewat koneksi yang sama, menggantikan fallback tanpa memuat ulang halaman atau permintaan jaringan kedua.
PPR masih berstatus fitur eksperimental, dan Next.js secara eksplisit menyarankan untuk tidak mengandalkannya di produksi sampai fitur ini stabil. Perlakukan setiap route yang mengadopsinya sebagai sesuatu yang bisa dibatalkan dengan bersih, dan uji ulang setiap kali meng-upgrade Next.js karena perilaku dasarnya bisa berubah antar rilis.
Contoh kerja lengkap
Potongan kode berikut menunjukkan hal minimum yang dibutuhkan untuk mengadopsi PPR pada satu route: flag konfigurasi, opt-in di level route, dan batas Suspense yang memisahkan header statis dari feed pesanan dinamis per pengguna.
| Bagian | Letaknya | Yang dikendalikan |
|---|---|---|
| ppr: incremental | next.config.ts | Membuka adopsi per route alih-alih pengaturan semua-atau-tidak-sama-sekali untuk seluruh proyek. |
| experimental_ppr = true | layout.tsx atau page.tsx | Mengaktifkan partial prerendering untuk segmen route ini beserta turunannya. |
| Batas Suspense | Komponen apa pun yang menggunakan cookies, headers, atau searchParams | Menandai lubang dinamis dan menyediakan fallback yang ditampilkan di shell statis. |
Di mana PPR sebenarnya membantu, dan di mana tidak
PPR pantas dengan kompleksitasnya pada route yang menggabungkan permukaan besar yang sebagian besar statis dengan sepotong kecil konten yang benar-benar personal. Dashboard yang sarat konten pemasaran, halaman produk dengan deskripsi statis ditambah jumlah stok yang berjalan langsung, atau layout blog dengan artikel statis dan jumlah komentar dinamis adalah kandidat yang baik.
- Paling cocok: halaman sarat konten di mana hanya satu widget kecil yang membutuhkan data langsung per permintaan, karena shell mendominasi waktu muat yang dirasakan pengguna.
- Paling cocok: route yang saat ini sepenuhnya dinamis hanya karena satu panggilan API dinamis kecil, di mana mengisolasi panggilan itu di belakang Suspense membuka shell yang bisa di-cache untuk sisanya.
- Kurang cocok: route yang dinamis hampir di semua tempat, di mana shell statisnya akan sangat kecil dan tambahan kompleksitas mengelola batas Suspense hanya memberi sedikit manfaat.
Perlu diingat juga bahwa PPR melengkapi, bukan menggantikan, alat rendering Next.js lainnya. Static rendering dengan revalidasi, dynamic rendering, dan PPR adalah tiga titik pada spektrum yang sama, dan memilih yang tepat adalah keputusan per route, bukan keputusan global.
Jebakan yang sering dialami tim saat mengadopsi PPR
Kesalahan paling umum adalah meletakkan batas Suspense terlalu tinggi di pohon komponen, mengelilingi seluruh bagian halaman padahal seharusnya hanya membungkus komponen spesifik yang membaca data waktu permintaan. Itu mengubah lubang dinamis yang kecil dan terarah menjadi lubang besar, dan UI fallback akhirnya menutupi konten yang sebenarnya bisa tetap statis. Turunkan batas tersebut ke komponen terkecil yang benar-benar perlu dinamis, dan biarkan semua di sekitarnya tetap menjadi bagian shell.
Jebakan kedua adalah melupakan bahwa data yang diambil di dalam komponen dinamis tetap membutuhkan strategi caching-nya sendiri. PPR mengendalikan kapan sebuah komponen dirender relatif terhadap permintaan, bukan apakah panggilan fetch di dalamnya di-cache. Komponen dinamis yang memanggil endpoint lambat tanpa cache di setiap permintaan akan tetap lambat saat di-streaming, walaupun bagian lain halaman sudah termuat secara instan. Padukan PPR dengan strategi caching fetch yang wajar atau memoisasi permintaan di dalam bagian dinamis itu sendiri.
- Jaga batas Suspense selalu seketat mungkin, hanya membungkus komponen spesifik yang membaca cookies, headers, atau searchParams.
- Berikan komponen dinamis loading state yang sesuai dengan ukurannya sendiri, bukan skeleton generik untuk seluruh halaman.
- Jalankan ulang build setelah mengadopsi PPR dan periksa output build untuk melihat route mana yang di-prerender sebagian versus penuh secara dinamis, karena komponen lebih tinggi di pohon bisa diam-diam membuat seluruh segmen menjadi dinamis penuh.

