karawaci.kode

← Semua snippet

TypeScript Lanjut Performance

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 pakai use() 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.tsx di route segment otomatis wrap children. Suspense manual lebih granular.
  • Error boundary — pasang error.tsx atau <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