nextjsloadingsuspenseMenengah4 mnt baca

Loading UI

Skeleton otomatis dengan loading.tsx dan Suspense.

Masalah: layar kosong saat data diambil

Halaman yang mengambil data dari server butuh waktu: query database, request API, komputasi berat. Tanpa penanganan khusus, pengguna melihat layar kosong, atau halaman lama yang tiba-tiba berganti tanpa aba-aba, pengalaman yang membingungkan.

Next.js menyediakan solusi berupa konvensi file: loading.tsx. File ini otomatis menjadi fallback yang ditampilkan saat segmennya sedang memuat, berkat mekanisme streaming di balik layar.

Contoh 1: loading.tsx paling sederhana

tsx
// app/blog/loading.tsx
export default function Loading() {
  return <p>Memuat artikel...</p>;
}

Cukup taruh file ini di app/blog/loading.tsx. Setiap kali pengguna navigasi ke route mana pun di bawah /blog yang butuh waktu memuat, Next.js langsung menampilkan "Memuat artikel..." sementara Server Component mengambil data di background. Begitu data siap, konten asli menggantikannya secara otomatis.

Mekanismenya: loading.tsx dibungkus Next.js dalam Suspense boundary otomatis untuk segmen tersebut. Kamu tidak perlu menulis <Suspense> sendiri untuk kasus dasar ini, konvensi nama file sudah cukup.

Contoh 2: skeleton yang meniru bentuk konten

Teks "memuat..." polos masih terasa murahan dan membuat layout "melompat" saat konten asli datang. Praktik terbaiknya adalah skeleton: placeholder abu-abu berdenyut yang meniru bentuk konten akhir:

tsx
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="space-y-4">
      {[1, 2, 3].map((i) => (
        <div key={i} className="h-24 animate-pulse rounded bg-gray-200 p-4">
          <div className="h-4 w-1/3 rounded bg-gray-300" />
          <div className="mt-2 h-3 w-2/3 rounded bg-gray-300" />
        </div>
      ))}
    </div>
  );
}

Tiga kartu skeleton dengan efek animate-pulse memberi kesan progres yang hidup. Karena bentuk dan ukurannya mirip kartu artikel asli, pergantian ke konten nyata terasa mulus tanpa layout shift yang mengganggu. Inilah alasan skeleton selalu lebih baik daripada spinner generik di tengah layar.

Kapan loading.tsx tidak cukup

loading.tsx berlaku untuk seluruh segmen sekaligus: selama ada bagian yang memuat, seluruh area segmen digantikan fallback. Kalau kamu ingin sebagian halaman tampil duluan (misalnya judul dan sidebar langsung muncul, hanya daftar artikel yang skeleton), pakai <Suspense> manual di dalam page:

tsx
// app/blog/page.tsx
import { Suspense } from "react";

export default function BlogPage() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<p>Memuat daftar artikel...</p>}>
        <ArticleList />
      </Suspense>
    </div>
  );
}

async function ArticleList() {
  await new Promise((r) => setTimeout(r, 2000));
  return (
    <ul>
      <li>Artikel 1</li>
      <li>Artikel 2</li>
    </ul>
  );
}

Di sini <h1>Blog</h1> langsung tampil, sementara ArticleList yang lambat dibungkus skeleton-nya sendiri. Pola ini disebut streaming granular dan dibahas lebih dalam di modul streaming dan Suspense.

Catatan: skeleton yang baik meniru bentuk konten akhir, bukan spinner generik. Dan ingat loading.tsx hanya muncul saat segmen benar-benar memuat (navigasi atau refresh data), bukan pada setiap render biasa. Untuk mengujinya, tambahkan delay buatan 2 detik di Server Component lalu throttle network di DevTools.

Tantangan

Skeleton blog

Buat halaman blog yang fetch data dengan delay buatan 2 detik, plus loading.tsx berupa skeleton 3 kartu. Throttle network di DevTools dan amati skeleton-nya.