Streaming dan Suspense di Next.js: Loading UI yang Terasa Cepat

Foto oleh Alan Cleaver on flickr
Streaming SSR mengirim HTML sebuah halaman dalam beberapa bagian melalui satu respons, bukan menunggu semua data selesai sebelum mengirim apa pun. Server rendering biasa memblokir seluruh respons pada pengambilan data yang paling lambat, sementara streaming membuat konten yang cepat muncul segera dan bagian yang lebih lambat mengisi setelahnya. Total beban kerja server relatif sama, tetapi waktu muat yang dirasakan membaik karena pengguna langsung melihat sesuatu yang bermakna.
File loading.tsx otomatis dibungkus di sekitar page dan layout bersarangnya dalam sebuah boundary Suspense, sehingga Next.js menampilkan fallback tersebut tepat saat navigasi dimulai, bukan membiarkan layar kosong. Fallback ini sering sudah di-prefetch sebelumnya, sehingga transisi terasa instan. Ini tidak mengurangi waktu pengambilan data yang sebenarnya, tetapi menghilangkan keheningan di mana pengguna tidak melihat apa pun terjadi.
Mulai setiap panggilan fetch independen sebelum menunggu salah satunya, lalu tunggu semua promise yang dihasilkan bersamaan dengan kombinator bersamaan seperti Promise.all, bukan menulis pernyataan await berurutan satu demi satu. Await berurutan memaksa panggilan jaringan yang tidak berkaitan berjalan bergiliran padahal tidak ada hasil yang bergantung pada yang lain. Hanya pertahankan fetch berurutan ketika memang membutuhkan data dari request sebelumnya.
Bisa, dan biasanya ini adalah pola yang lebih baik begitu sebuah halaman memiliki lebih dari satu bagian independen. Membungkus header, widget komentar, dan panel rekomendasi dalam boundary Suspense yang terpisah membuat setiap bagian mengalir masuk dengan waktunya sendiri, sehingga satu bagian yang lambat tidak menahan bagian lain untuk muncul. Boundary juga bisa disusun bersarang untuk pengungkapan progresif, menampilkan fallback besar dahulu lalu fallback kecil untuk sub bagian yang lebih lambat.
Hook use milik React memungkinkan Client Component membaca nilai akhir dari sebuah promise yang dibuat di Server Component dan diteruskan sebagai prop tanpa ditunggu terlebih dahulu. Membungkus komponen penerima dalam boundary Suspense membuat fallback muncul otomatis selama promise masih tertunda dan berganti ke konten asli begitu selesai. Ini berbeda dari mengambil data di dalam effect hook, karena pengambilan data berbasis effect tidak pernah memicu fallback Suspense pada tampilan pertama.

Foto oleh Alan Cleaver on flickr
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.
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.
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.
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>
)
}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 |
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.
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.