- Sebelumnya satu halaman harus memilih: seluruhnya statis ATAU seluruhnya dinamis
- PPR mengirim kerangka statis lebih dulu, lalu mengalirkan bagian dinamis
- Suspense menentukan batas antara bagian statis dan dinamis
- Fallback Suspense yang buruk menyebabkan pergeseran tata letak
π Masalah yang Diselesaikan
Halaman produk berisi:
ββ Header, navigasi, footer β sama untuk semua orang (statis)
ββ Deskripsi & gambar produk β jarang berubah (statis)
ββ Harga & stok terkini β harus segar (dinamis)
ββ "Direkomendasikan untuk Anda" β per pengguna (dinamis)
Model lama: satu bagian dinamis membuat SELURUH halaman
dirender ulang tiap permintaan. Header yang tidak pernah
berubah pun ikut menunggu kueri stok selesai.
Partial Prerendering memecah asumsi itu. Kerangka statis sudah jadi sejak waktu build dan dikirim seketika; lubang-lubang dinamisnya diisi menyusul lewat streaming dalam respons yang sama.
π» Suspense sebagai Penanda Batas
import { Suspense } from 'react'
export default async function HalamanProduk({ params }) {
const { id } = await params // params adalah Promise di Next 15+
const produk = await ambilProduk(id) // ber-'use cache' β statis
return (
<main>
{/* Semua ini masuk kerangka statis */}
<h1>{produk.nama}</h1>
<GambarProduk src={produk.gambar} />
<p>{produk.deskripsi}</p>
{/* Lubang dinamis: dialirkan menyusul */}
<Suspense fallback={<SkeletonHarga />}>
<HargaDanStok id={id} />
</Suspense>
<Suspense fallback={<SkeletonRekomendasi />}>
<Rekomendasi untukPengguna />
</Suspense>
</main>
)
}
// Komponen ini membaca data segar tiap permintaan
async function HargaDanStok({ id }: { id: string }) {
const { harga, stok } = await db.produk.harga(id)
return <div>Rp{harga} Β· sisa {stok}</div>
}
Apa pun yang berada di dalam <Suspense> dan menyentuh data dinamis β cookies(), headers(), searchParams, atau kueri tanpa cache β menjadi lubang dinamis. Sisanya masuk kerangka statis. Anda mengendalikan pembagiannya lewat penempatan batas Suspense.
π API Dinamis Kini Asinkron
// Next 14 β sinkron
const cookieStore = cookies()
const token = cookieStore.get('token')
// Next 15+ β WAJIB di-await
const cookieStore = await cookies()
const token = cookieStore.get('token')
const headersList = await headers()
const { id } = await params
const { q } = await searchParams
cookies(), headers(), params, dan searchParams menjadi asinkron sejak Next 15. Perubahan ini yang memungkinkan Next menunda evaluasi bagian dinamis sampai kerangka statis terkirim. Kode dari tutorial Next 14 akan gagal atau memberi peringatan. Tersedia codemod untuk migrasi otomatis:
npx @next/codemod@latest next-async-request-api .
π¨ Fallback yang Baik Menentukan Kualitas
// β Menyebabkan pergeseran tata letak β tinggi berubah saat data tiba
<Suspense fallback={<p>Memuatβ¦</p>}>
// β
Skeleton berukuran sama dengan konten aslinya
<Suspense fallback={
<div className="h-14 w-40 animate-pulse rounded bg-neutral-200" />
}>
Streaming memperbaiki waktu tampil pertama, tetapi bila fallback berukuran berbeda dari konten aslinya, halaman akan "meloncat" saat data tiba. Pengguna yang sedang membaca atau hendak menekan tombol akan terganggu, dan skor Core Web Vitals Anda turun. Buat skeleton dengan dimensi yang sama persis dengan konten yang akan menggantikannya.
βοΈ Mengaktifkan & Memeriksa
// next.config.ts β nama flag berbeda antar versi,
// periksa dokumentasi untuk versi yang Anda pakai
export default {
experimental: {
ppr: 'incremental', // aktifkan per-rute
},
}
// lalu pada rute yang diinginkan:
export const experimental_ppr = true
# Melihat rute mana yang statis, dinamis, atau parsial
npm run build
# Keluarannya menandai tiap rute:
# β Static β dirender saat build
# β SSG β statis dengan generateStaticParams
# Ζ Dynamic β dirender tiap permintaan
# β Partial β kerangka statis + lubang dinamis
Satu pemanggilan cookies() yang tidak sengaja berada di luar Suspense akan mengubah seluruh rute menjadi dinamis, dan Anda kehilangan manfaat prerendering tanpa peringatan apa pun. Tabel rute di akhir next build adalah cara tercepat menangkapnya β jadikan kebiasaan memeriksanya sebelum deploy.