RSC streaming dengan Suspense boundary
Pakai React Server Component + Suspense boundary supaya bagian page yang cepet langsung tampil, sisanya stream belakangan. TTFB drop drastis.
Dipublikasikan 11 Juni 2026
E-commerce page punya banyak komponen: header (cepet), produk detail (cepet), review (lambat — call API rating service), rekomendasi (lambat — call ML service). Tanpa streaming, user nunggu 2 detik sampai semua kelar. Dengan Suspense boundary, header dan produk detail tampil dalam 200ms, sisanya nyusul.
Kode
// app/produk/[id]/page.tsx
import { Suspense } from "react";
import { ProdukDetail } from "./ProdukDetail";
import { Rekomendasi } from "./Rekomendasi";
import { ReviewList } from "./ReviewList";
import { SkeletonReview, SkeletonRekomendasi } from "./Skeleton";
export default async function ProdukPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<main>
{/* ProdukDetail di-await langsung — fast query (~50ms) */}
<ProdukDetail produkId={id} />
{/* Review pakai Suspense — slow API, stream belakangan */}
<section aria-label="Review pembeli">
<h2>Review pembeli</h2>
<Suspense fallback={<SkeletonReview />}>
<ReviewList produkId={id} />
</Suspense>
</section>
{/* Rekomendasi juga di-defer */}
<section aria-label="Produk serupa">
<h2>Produk serupa</h2>
<Suspense fallback={<SkeletonRekomendasi />}>
<Rekomendasi produkId={id} />
</Suspense>
</section>
</main>
);
}
// app/produk/[id]/ProdukDetail.tsx — fast component, no Suspense
import { db } from "@/lib/db";
import Image from "next/image";
export async function ProdukDetail({ produkId }: { produkId: string }) {
// Cepat — index DB by primary key
const produk = await db.query.produk.findFirst({
where: (p, { eq }) => eq(p.id, Number(produkId)),
});
if (!produk) {
return <div>Produk tidak ditemukan</div>;
}
return (
<article>
<Image
src={produk.foto}
alt={produk.nama}
width={400}
height={400}
priority
/>
<h1>{produk.nama}</h1>
<p className="harga">Rp {produk.harga.toLocaleString("id-ID")}</p>
<p>{produk.deskripsi}</p>
</article>
);
}
// app/produk/[id]/ReviewList.tsx — slow, di Suspense
interface Review {
id: number;
nama: string;
rating: number;
komentar: string;
}
async function fetchReviews(produkId: string): Promise<Review[]> {
// Slow external API (~800ms)
const r = await fetch(`https://api.review-service.id/produk/${produkId}`, {
next: { revalidate: 60 },
});
if (!r.ok) throw new Error("Review service down");
return r.json();
}
export async function ReviewList({ produkId }: { produkId: string }) {
const reviews = await fetchReviews(produkId);
return (
<ul>
{reviews.map((r) => (
<li key={r.id}>
<strong>{r.nama}</strong> — {r.rating}/5
<p>{r.komentar}</p>
</li>
))}
</ul>
);
}
// app/produk/[id]/Rekomendasi.tsx — slow, di Suspense
interface ProdukMirip {
id: number;
nama: string;
harga: number;
}
async function fetchRekomendasi(produkId: string): Promise<ProdukMirip[]> {
// Slow ML service (~1.5s)
const r = await fetch(`https://ml.example.com/serupa/${produkId}`, {
next: { revalidate: 300 },
});
if (!r.ok) return [];
return r.json();
}
export async function Rekomendasi({ produkId }: { produkId: string }) {
const items = await fetchRekomendasi(produkId);
return (
<ul className="grid">
{items.map((p) => (
<li key={p.id}>
<a href={`/produk/${p.id}`}>
{p.nama}
<p>Rp {p.harga.toLocaleString("id-ID")}</p>
</a>
</li>
))}
</ul>
);
}
// app/produk/[id]/Skeleton.tsx
export function SkeletonReview() {
return (
<ul aria-busy="true">
{Array.from({ length: 3 }, (_, i) => (
<li key={i} className="skeleton-row">
<div className="skeleton-bar w-1/3" />
<div className="skeleton-bar w-full" />
</li>
))}
</ul>
);
}
export function SkeletonRekomendasi() {
return (
<div className="grid grid-cols-4 gap-3" aria-busy="true">
{Array.from({ length: 4 }, (_, i) => (
<div key={i} className="skeleton-card" />
))}
</div>
);
}
Kapan dipakai
- E-commerce product detail page dengan banyak data source.
- Dashboard analytics — chart cepat tampil, ML insight nyusul.
- Marketplace listing — produk utama tampil, filter sidebar belakangan.
- Berita / artikel dengan komentar dan related article.
Catatan
- Suspense butuh async boundary — pakai di Server Component yang
async. Client Component pakaiuse()hook untuk Suspense. - Streaming butuh HTTP/2 atau chunked transfer encoding. Cek edge runtime (Vercel, Cloudflare) sudah enable secara default.
- Loading.tsx vs explicit Suspense —
loading.tsxdi route segment otomatis wrap children. Suspense manual lebih granular. - Error boundary — pasang
error.tsxatau<ErrorBoundary>di sebelah Suspense biar partial error gak crash whole page. - Don’t await everything at top — kalau
await fetchReviews()di parent component (bukan child Suspense), streaming gak jalan. Push await ke component anak.
Jangan streaming untuk halaman yang butuh full HTML upfront (PDF print, RSS feed, og:image scraper). Crawler social media kadang gak follow streaming dengan baik untuk meta tag.
# tags
rscsuspensestreamingnextjsperformance
Ditulis oleh Asti Larasati · 11 Juni 2026