Streaming dengan Suspense
Render bertahap: bagian cepat tampil dulu, yang lambat menyusul.
Masalah: halaman menunggu komponen paling lambat
Bayangkan restoran yang menyajikan makanan hanya kalau SEMUA pesanan satu meja sudah matang. Satu steak yang lama membuat seluruh meja kelaparan, padahal salad dan sup sudah siap dari tadi. Itulah perilaku default halaman web: browser menunggu seluruh data selesai di-fetch sebelum menampilkan apa pun.
Halaman dashboard tipikal butuh tiga data: profil user (cepat, 200ms), statistik (lambat, 2 detik), grafik analitik (sangat lambat, 5 detik). Tanpa streaming, user menatap spinner selama 5 detik penuh, padahal profilnya sudah siap sejak detik pertama.
Solusinya: Suspense sebagai pelayan bertahap
React Suspense membagi halaman menjadi zona-zona independen. Setiap zona menampilkan fallback (skeleton atau spinner) selagi datanya dimuat, lalu "menyusul" begitu datanya siap, tanpa menunggu zona lain. Next.js mengirim HTML-nya bertahap dalam satu response, teknologi di baliknya kadang disebut streaming SSR.
Syaratnya satu: komponen di dalam <Suspense> harus berupa async Server Component yang melakukan await data. Next.js menunda render zona itu, mengirim sisa halaman duluan, lalu menambal hasilnya begitu promise selesai.
Contoh 1: dashboard dengan tiga kecepatan
// app/dashboard/page.tsx
import { Suspense } from "react";
import ProfileCard from "./ProfileCard";
import StatsPanel from "./StatsPanel";
import AnalyticsChart from "./AnalyticsChart";
import { StatsSkeleton, ChartSkeleton } from "./skeletons";
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/* Cepat: langsung tampil, tidak perlu dibungkus Suspense */}
<ProfileCard />
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel /> {/* lambat, menyusul belakangan */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart /> {/* paling lambat, tampil terakhir */}
</Suspense>
</main>
);
}// app/dashboard/StatsPanel.tsx
async function getStats() {
await new Promise((r) => setTimeout(r, 2000)); // simulasi query lambat
return { visitors: 12840, sales: 312 };
}
export default async function StatsPanel() {
const stats = await getStats();
return (
<section>
<h2>Statistik</h2>
<p>Pengunjung: {stats.visitors}</p>
<p>Penjualan: {stats.sales}</p>
</section>
);
}Perhatikan ProfileCard tidak dibungkus Suspense: karena datanya cepat, ia langsung tampil bersama kerangka halaman. Hanya zona lambat yang diberi fallback sendiri.
Contoh 2: skeleton fallback yang rapi
Fallback yang baik menyerupai bentuk konten aslinya (skeleton), bukan spinner generik yang membuat layout meloncat-loncat:
// app/dashboard/skeletons.tsx
export function StatsSkeleton() {
return (
<section aria-busy="true" aria-label="Memuat statistik">
<div className="skeleton" style={{ height: 24, width: "60%" }} />
<div className="skeleton" style={{ height: 24, width: "40%" }} />
</section>
);
}
export function ChartSkeleton() {
return (
<section aria-busy="true" aria-label="Memuat grafik">
<div className="skeleton" style={{ height: 200 }} />
</section>
);
}Atribut aria-busy memberi tahu screen reader bahwa area ini sedang dimuat, detail kecil yang membuat aplikasimu lebih aksesibel.
loading.tsx vs Suspense manual: kapan pakai yang mana
loading.tsx: satu fallback untuk seluruh segmen route. Cocok untuk momen navigasi antar halaman, karena tampil otomatis setiap pindah route.<Suspense>manual: fallback per komponen. Cocok untuk halaman yang sebagian kontennya lambat, karena bagian cepat tetap tampil duluan.
Keduanya bisa digabung: loading.tsx untuk transisi pindah halaman, Suspense manual untuk zona lambat di dalam halaman. Aturan praktisnya: bungkus hanya komponen yang benar-benar lambat. Membungkus semua komponen dengan Suspense justru membuat kode berisik tanpa manfaat yang terasa.
Tantangan
Dashboard streaming
Buat halaman dengan 3 komponen ber-delay berbeda (1s, 2s, 3s), masing-masing dibungkus Suspense dengan fallback sendiri. Amati urutan tampilnya.
Kuis Bab
Uji pemahamanmu: Layout & UI States
Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".
1.Apa perbedaan utama template.tsx vs layout.tsx?
2.Kapan loading.tsx tampil?
3.Syarat wajib file error.tsx adalah...
4.Apa kegunaan <Suspense> manual dibanding loading.tsx?
5.Kapan global-error.tsx dipakai?