Suspense menampilkan fallback, seperti pesan loading, selama komponen di dalamnya belum siap. React.lazy adalah hal yang paling sering ditunggu: ia baru memuat kode sebuah komponen saat komponen itu pertama kali dirender, sehingga unduhan awal tetap kecil.
Klik Show chart: Loading chart... muncul selama satu detik, lalu grafiknya. Sembunyikan lalu tampilkan lagi, dan grafiknya langsung muncul tanpa baris log baru, karena lazy menyimpan modul yang sudah dimuat.
Code splitting dengan lazy
Editor ini memuat semuanya dalam satu file, jadi contohnya membuat modul lambat itu secara manual: promise yang selesai menjadi objek dengan export default setelah satu detik. Di aplikasi sungguhan, komponennya berada di file sendiri dan kamu mengirim dynamic import:
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart.jsx'));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
);
}
import('./Chart.jsx') mengembalikan promise untuk modul itu. Bundler seperti Vite dan webpack melihat dynamic import tersebut lalu menaruh Chart.jsx dan semua yang hanya dipakai olehnya ke file terpisah, yang diunduh saat <Chart /> pertama kali dirender. Aturan yang perlu diketahui:
- Modulnya butuh default export.
lazymembaca propertydefaultdari nilai hasil promise. Untuk named export, petakan:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))). - Panggil
lazydi level teratas modul. Di dalam komponen, ia akan membuat tipe komponen baru di setiap render, sehingga React akan melakukan unmount pada yang lama, kehilangan state-nya, dan memuatnya lagi. - Pecah di tempat pengguna memang menunggu. Route, modal, panel yang jarang dibuka, dan widget berat (editor, grafik, peta) adalah kandidat yang bagus. Memecah setiap komponen kecil hanya menambah request dan state loading tanpa manfaat.
Preload sebelum diklik
Komponen lazy mulai dimuat saat pertama kali dirender, jadi pengguna selalu menunggu setidaknya satu unduhan setelah mengklik. Jika kamu bisa menebak bahwa klik akan terjadi, mulai unduhannya lebih awal. Simpan fungsi import di variabel lalu panggil saat hover atau fokus; browser menyimpan modulnya, jadi saat lazy memanggil import yang sama nanti, hasilnya langsung tersedia tanpa unduhan kedua.
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Cara Suspense memutuskan apa yang ditampilkan
Saat sebuah komponen di dalam <Suspense> belum siap, komponen itu melakukan suspend: React berhenti merender bagian itu dan menampilkan fallback dari Suspense terdekat di atasnya. Semua yang ada di dalam boundary itu digantikan fallback, bukan hanya komponen yang sedang menunggu. Saat yang ditunggu sudah siap, React merender kontennya lagi dan menggantikan fallback.
Karena itu, posisi boundary adalah keputusan desain. Taruh bagian-bagian yang berdiri sendiri di boundary masing-masing agar setiap bagian bisa muncul saat sudah siap:
Awalnya seluruh halaman menampilkan Loading page..., karena Header termasuk boundary luar. Begitu header siap, artikel muncul dengan Loading comments... di bawahnya, dan komentar datang paling akhir. Ganti 1500 menjadi 3000, dan pratinjau dimuat ulang dengan waktu tunggu lebih lama hanya untuk komentar. Hapus <Suspense> bagian dalam (pertahankan <Comments />), dan halaman menunggu komentar sebelum menampilkan apa pun.
Suspense dengan use() di React 19
Di React 19, sebuah komponen bisa membaca promise dengan use(promise). Jika promise masih pending, komponen itu melakukan suspend dan Suspense terdekat menampilkan fallback-nya; saat promise selesai, use mengembalikan nilainya. Halaman hook use membahasnya secara lengkap. fetchUser palsu di bawah ini menggantikan request sungguhan.
Klik setiap user, lalu kembali ke User 1: profilnya langsung muncul dan Konsol tidak mencatat fetch baru, karena promise-nya sudah ada di cache.
Kenapa promise harus disimpan di cache
Map cache di sini bukan sekadar optimasi, tetapi wajib. Komponen yang melakukan suspend tidak menyimpan apa pun dari percobaan itu: saat promise selesai, React merendernya lagi dari awal. Jika Profile memanggil fetchUser(id) secara langsung, setiap percobaan akan membuat promise baru, memulai request baru, lalu suspend lagi. Profilnya tidak pernah muncul, dan Konsol dipenuhi baris fetching user. Jadi promise-nya harus berasal dari tempat yang bertahan lebih lama dari render:
- cache yang memakai request sebagai key, seperti map di atas (library data seperti TanStack Query dan loader framework melakukan ini untukmu);
- induk yang membuat promise sekali, di event handler atau Server Component, lalu mengirimnya ke bawah sebagai prop.
Perhatikan bahwa setiap user baru tetap mengganti profil dengan fallback. Jika kamu ingin user lama tetap tampil di layar sampai user berikutnya siap, bungkus update-nya dengan transition: startTransition(() => setId(n)). React tidak menyembunyikan konten yang sudah terlihat demi sebuah transition (lihat useTransition).
Error butuh error boundary
Suspense menangani proses menunggu, bukan kegagalan. Jika lazy import gagal (pengguna sedang offline, deploy baru menghapus chunk lama) atau promise yang dikirim ke use ditolak, React melempar error itu ke error boundary terdekat. Tanpa error boundary, seluruh tree di bawah root akan unmount. Error boundary masih berupa class component (lihat error boundary):
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
Yang tidak terdeteksi Suspense
Suspense hanya merespons komponen yang melakukan suspend: komponen lazy, use(promise), dan sumber data yang dibuat untuk Suspense (loader framework, library yang mendukung Suspense). Fetch di dalam useEffect yang mengatur state saat selesai tidak melakukan suspend, jadi boundary Suspense di sekitarnya tidak pernah menampilkan fallback-nya. Untuk pola itu, kamu menyimpan state loading sendiri, seperti yang ditunjukkan di halaman fetch data.
Pertanyaan yang sering diajukan
Apa itu React Suspense?
<Suspense fallback={...}> adalah komponen yang menampilkan fallback-nya selama ada komponen di dalamnya yang sedang menunggu sesuatu, seperti kode yang dimuat secara lazy atau data yang dibaca dengan use. Saat semua isinya siap, React menukar fallback dengan kontennya.
Apa fungsi React.lazy?
lazy(() => import('./Chart.jsx')) membuat komponen yang kodenya diunduh saat pertama kali dirender. Bundler menaruh file itu di chunk terpisah, sehingga halaman awal memuat lebih sedikit JavaScript.
Apakah Suspense berfungsi untuk fetch data?
Ya, jika sumber datanya mendukung. Di React 19, sebuah komponen bisa membaca promise dengan use(promise) dan melakukan suspend sampai promise itu selesai. Framework seperti Next.js juga mengintegrasikan Suspense dengan pemuatan datanya. Fetch di dalam useEffect tidak memicu Suspense.
Bagaimana cara menangani error dengan Suspense?
Suspense hanya menangani proses menunggu. Jika lazy import atau promise gagal, error-nya dikirim ke error boundary terdekat, jadi bungkus boundary Suspense (atau induknya) dengan error boundary.
Di mana sebaiknya saya memanggil lazy?
Di level teratas modul, di luar komponen mana pun. Memanggil lazy di dalam komponen membuat tipe komponen baru di setiap render, yang mengatur ulang state-nya dan memuatnya lagi.