Ringkasan Utama
Streaming server rendering dengan React Suspense memungkinkan Next.js langsung mengirim bagian halaman yang sudah siap dan menyusulkan bagian yang lebih lambat begitu datanya selesai diambil, menggantikan satu render yang memblokir semuanya dengan boundary per bagian, pengambilan data paralel, dan hook use, sehingga waktu muat yang dirasakan membaik tanpa mengurangi total beban kerja server.
Server rendering tradisional bersifat semua atau tidak sama sekali. Server mengumpulkan setiap data yang dibutuhkan halaman, menyusun seluruh dokumen HTML, dan baru setelah itu mengirim satu respons ke browser. Jika satu query lambat, seluruh halaman menunggu di belakangnya, dan pengunjung menatap tab kosong sambil bertanya-tanya apakah ada sesuatu yang sedang terjadi.
Streaming server rendering yang dipadukan dengan React Suspense membalikkan model tersebut. Server mengirim bagian halaman yang sudah siap segera, lalu mengalirkan sisanya begitu setiap bagian selesai diproses, dalam respons HTTP yang sama. Total beban kerja server tidak berkurang, tetapi halaman terasa jauh lebih cepat karena pengguna melihat sesuatu yang bermakna dalam hitungan milidetik, bukan menunggu query paling lambat selesai.
Cara Lama: Satu Render Besar yang Memblokir
Pada pendekatan klasik fetch lalu render, komponen halaman menunggu setiap panggilan data selesai sebelum mengembalikan markup apa pun. Framework tidak bisa mengirim satu byte pun dari respons sampai setiap promise dalam fungsi tersebut selesai, yang berarti dependensi paling lambat menentukan time to first byte untuk seluruh rute, bukan hanya bagian yang membutuhkannya.
Pengguna mengalami ini sebagai layar putih kosong yang diikuti kemunculan tiba-tiba halaman yang sudah jadi. Meski total waktu pemrosesan wajar, tidak adanya progres yang terlihat terasa seperti sistem yang rusak atau lambat, dan pada koneksi seluler yang lambat, keheningan itu memanjang dan memperparah frustrasi.
loading.tsx: Suspense Otomatis Per Segmen Rute
Next.js memberikan setiap segmen rute cara ringan untuk mengaktifkan streaming tanpa menyentuh satu pun elemen Suspense secara langsung. Letakkan file loading di samping file page pada App Router, dan framework otomatis menyisipkannya di dalam layout, membungkus page beserta layout bersarang di bawahnya dalam sebuah boundary yang menampilkan fallback begitu navigasi dimulai.
Karena fallback sering sudah di-prefetch sebelumnya, navigasi terasa instan bahkan sebelum rute tujuan selesai dirender di server. Isi respons sendiri mulai mengalir tepat saat fallback dirender atau komponen di bawahnya menunda diri, dan server tetap mengembalikan status kode sukses biasa, karena header sudah dikirim sebelum bagian yang lambat selesai.
- Jaga fallback tetap ringan, skeleton atau versi sebagian dari tampilan akhir bekerja jauh lebih baik dibanding spinner polos yang menutupi seluruh layar
- File loading membungkus page dan layout bersarang di bawahnya, tetapi tidak membungkus file template atau error yang sejajar dalam segmen yang sama
- Jika sebuah layout membaca cookies, headers, atau data lain yang tidak di-cache di luar boundary-nya sendiri, navigasi tetap terblokir olehnya, jadi pindahkan pengambilan data itu ke page atau beri boundary tersendiri
- Mesin pencari yang tidak dapat menjalankan JavaScript tetap menerima metadata halaman yang sudah selesai diproses di dalam head dokumen sebelum streaming dimulai, jadi teknik ini tidak merugikan sisi SEO sama sekali
Boundary Suspense Granular Lebih Baik daripada Satu Spinner Besar
Satu file loading mencakup seluruh rute, yang merupakan titik awal yang baik tetapi menjadi alat yang terlalu kasar begitu sebuah halaman memiliki beberapa bagian independen. Header, widget komentar, dan panel rekomendasi tidak saling bergantung, jadi membungkus setiap bagian dalam boundary-nya sendiri membuat bagian yang cepat langsung dirender sementara hanya bagian yang lambat menampilkan fallback.
Boundary juga bisa disusun bersarang untuk pengungkapan progresif: boundary luar menampilkan fallback besar sampai konten utama siap, dan boundary dalam di sekitar sub bagian yang lebih lambat tetap menampilkan fallback kecilnya sendiri setelah konten luar muncul. Boundary yang selesai dalam rentang waktu yang berdekatan cenderung muncul bersamaan daripada muncul satu per satu, sehingga halaman tidak terasa patah-patah.
export default function DashboardPage() {
return (
<section>
<Header />
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</section>
)
}Menghilangkan Request Waterfall dengan Parallel Fetching
Request waterfall terjadi ketika panggilan jaringan yang tidak saling berkaitan berjalan satu demi satu murni karena cara kode ditulis, bukan karena salah satu hasil benar-benar bergantung pada yang lain. Menunggu satu panggilan lalu memulai panggilan berikutnya di dalam fungsi async yang sama membuat kedua perjalanan bolak-balik jaringan itu berurutan, padahal kedua data tersebut tidak ada hubungannya sama sekali.
// Waterfall: albums cannot start until artist resolves
const artist = await getArtist(username)
const albums = await getAlbums(username)
// Parallel: both requests are in flight before either is awaited
const artistData = getArtist(username)
const albumsData = getAlbums(username)
const [artist, albums] = await Promise.all([artistData, albumsData])Solusinya adalah memulai kedua panggilan sebelum menunggu salah satunya, lalu menunggu keduanya bersamaan, sehingga keduanya berjalan bersamaan bukan bergiliran. Satu catatan penting: jika setiap request harus berhasil agar halaman berguna, gunakan kombinator ketat yang gagal total jika salah satu gagal; jika hasil sebagian masih bisa diterima, gunakan versi yang menyelesaikan setiap promise secara individual dan tidak pernah menolak seluruh batch. Dan ketika sebuah fetch memang membutuhkan hasil dari fetch sebelumnya, simpan bagian yang bergantung itu dalam boundary-nya sendiri agar tidak menahan bagian halaman lain yang tidak perlu menunggu.
| Pola | Apa yang Terjadi | Kapan Digunakan |
|---|---|---|
| Await berurutan | Request kedua tidak bisa dimulai sampai request pertama selesai | Request kedua benar-benar membutuhkan data yang dihasilkan request pertama |
| Kombinator promise bersamaan | Request independen dimulai bersamaan dan seluruh grup selesai secepat request yang paling lambat | Request sama sekali tidak bergantung satu sama lain |
| Satu boundary per bagian | Sepotong UI independen mengalir masuk dengan waktunya sendiri, terpisah dari bagian halaman lainnya | Satu bagian lambat yang tidak boleh menahan bagian lain untuk muncul |
Streaming Client Component dengan Hook use
Sebuah server component dapat memanggil fungsi data async tanpa menunggunya, lalu menyerahkan promise yang belum selesai itu langsung ke client component sebagai prop, membungkus komponen penerima dalam boundary Suspense. Di dalam client component, hook use milik React membaca nilai akhir dari promise tersebut, dan boundary di sekitarnya otomatis menampilkan fallback sampai promise selesai.
Ini adalah model mental yang cukup berbeda dari mengambil data di dalam effect hook. Effect hanya berjalan setelah komponen selesai dipasang, sehingga tampilan pertama selalu kosong untuk bagian itu. Membaca promise dengan hook use terintegrasi langsung dengan Suspense, sehingga fallback muncul segera dan boundary berganti otomatis begitu data asli siap, tanpa perlu melacak status loading secara manual.
Rancang setiap fallback agar menempati ruang yang kurang lebih sama dengan konten yang akan menggantikannya. Cadangkan rasio aspek gambar, beri skeleton tinggi baris yang sama dengan teks aslinya, dan pergantian akan terasa seperti pengungkapan yang mulus, bukan lompatan tata letak yang mengejutkan.
Beberapa browser berbasis WebKit menahan awal respons streaming sampai melewati ambang ukuran tertentu, sehingga halaman demo kecil bisa terlihat termuat sekaligus bukan secara progresif. Ini adalah perilaku bawaan browser, bukan server yang rusak, jadi selalu nilai perilaku streaming pada halaman nyata dengan payload yang realistis, bukan contoh hello world yang sepele.
Checklist Sebelum Dirilis
- Beri setiap bagian yang benar-benar lambat boundary-nya sendiri daripada mengandalkan satu file loading untuk seluruh rute
- Jalankan pemanggilan data independen melalui kombinator bersamaan, bukan merangkai pernyataan await secara berurutan
- Hanya menyusun boundary bersarang ketika data di dalamnya memang bergantung pada hasil boundary luar yang selesai lebih dulu
- Jaga setiap fallback tetap mendekati dimensi konten akhir agar tidak ada yang melompat begitu data asli tiba
- Uji pada profil jaringan yang dibatasi, karena server pengembangan lokal yang cepat menyembunyikan kerusakan akibat waterfall yang baru terlihat setelah ada latensi nyata
Streaming dan Suspense tidak membuat server bekerja lebih sedikit, keduanya mengubah metrik mana yang paling penting. Total waktu muat berhenti menjadi satu-satunya angka yang layak dioptimalkan, dan waktu muat yang dirasakan, seberapa cepat pengguna melihat sesuatu yang bermakna, menjadi hal yang sebenarnya sedang dikejar.
Gabungkan boundary otomatis yang diberikan loading.tsx secara gratis dengan Suspense yang diletakkan manual di sekitar bagian yang benar-benar lambat, ambil data independen secara bersamaan, dan ukur hasilnya pada koneksi yang dibatasi sebelum menganggapnya selesai. Kombinasi itulah yang mengubah halaman yang secara teknis benar menjadi halaman yang terasa cepat.

