Next.js Partial Prerendering: Shell Statis, Lubang Dinamis

Foto oleh markus spiske on flickr
Partial Prerendering, atau PPR, adalah strategi rendering yang menggabungkan shell yang di-prerender secara statis dengan konten dinamis yang di-streaming saat permintaan tiba, semuanya dalam satu route. Bagian statis halaman di-cache dan disajikan secara instan, sementara komponen yang dibungkus Suspense dan bergantung pada data waktu permintaan dirender sesuai kebutuhan dan di-streaming ke halaman lewat respons yang sama.
Atur opsi ppr menjadi incremental di next.config, lalu ekspor experimental_ppr bernilai true dari file layout atau page pada setiap route yang ingin diikutkan. Ekspor tersebut berlaku turun ke layout dan page bersarang di bawah segmen itu, sehingga hanya perlu dideklarasikan sekali per route.
Tidak. Suspense hanya membuat sebuah batas yang bisa menampilkan fallback selama konten dimuat; ia tidak membuat komponen menjadi dinamis dengan sendirinya. Komponen menjadi dinamis ketika membaca API waktu permintaan seperti cookies, headers, atau searchParams, dan Suspense adalah yang memungkinkan perilaku dinamis itu dikurung tanpa memaksa seluruh route dirender secara dinamis.
Next.js saat ini mendokumentasikan PPR sebagai fitur eksperimental dan secara eksplisit menyarankan untuk tidak mengandalkannya di produksi sampai stabil. Tim yang mencobanya sebaiknya memvalidasi dengan cermat dan menguji ulang setiap kali meng-upgrade Next.js, karena perilaku eksperimental bisa berubah antar rilis.
PPR paling membantu pada route di mana sebagian besar halaman statis berisi sepotong kecil konten yang benar-benar personal, seperti dashboard dengan feed pesanan langsung atau halaman produk dengan jumlah stok yang berjalan langsung. Pada route yang dinamis hampir di semua tempat, shell statisnya jadi sangat kecil dan tambahan kompleksitas mengelola batas Suspense hanya memberi sedikit manfaat.

Foto oleh markus spiske on flickr
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.
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.
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.
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>
</>
)
}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.
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.
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. |
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.
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.
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.