Tanpa pengaturan apa-apa, seluruh JavaScript app dikirim dalam satu file besar (bundle), dan halaman baru tampil setelah file itu selesai diunduh. lazy memisahkan satu komponen ke file sendiri (chunk) yang baru diunduh waktu komponen itu pertama kali dirender, dan Suspense menampilkan tampilan sementara (fallback) selama chunk-nya masih di jalan. Bayangin warung makan. Nasi dan lauk utama sudah siap di etalase, jadi pembeli langsung dilayani. Es campur baru dibuat waktu ada yang pesan, dan selama dibuat pelayannya bilang "sebentar ya". Begitu bahan es campur sudah keluar dari kulkas, pesanan berikutnya bisa langsung jadi.
🧪 Tebakan cepat:
HeavyChartsudah pernah dimuat sekali denganlazy, lalu disembunyikan, lalu ditampilkan lagi. Apakah fallbackSedang memuat...muncul lagi? Tebak dulu, jawabannya ada di bagian "Kedua kalinya langsung tampil".
Satu bundle besar, atau dipecah
Misalnya app dashboard-mu berukuran 300 KB, dan 180 KB di antaranya komponen grafik HeavyChart yang baru dipakai setelah tombol diklik.
| Cara | Diunduh sebelum halaman tampil | Diunduh nanti |
|---|---|---|
| Satu bundle | 300 KB | gak ada |
HeavyChart di-lazy |
120 KB | 180 KB, waktu grafiknya pertama kali dirender |
Di koneksi sekitar 200 KB per detik, 300 KB butuh kira-kira 1,5 detik, sedangkan 120 KB cuma 0,6 detik. Pengguna yang gak pernah membuka grafik gak pernah mengunduh 180 KB itu sama sekali. Teknik memecah bundle seperti ini disebut code splitting.
lazy dan Suspense
import { lazy, Suspense, useState } from "react"
const HeavyChart = lazy(() => import("./HeavyChart"))
function App() {
const [tampil, setTampil] = useState(false)
return (
<div>
<button onClick={() => setTampil(true)}>Muat Komponen</button>
{tampil && (
<Suspense fallback={<p id="fallback">Sedang memuat...</p>}>
<HeavyChart />
</Suspense>
)}
</div>
)
}
import("./HeavyChart")adalah dynamic import: mengunduh file itu waktu dipanggil, dan mengembalikan promise. Tool build seperti Vite otomatis menjadikannya chunk terpisah.lazy(() => import(...))membuat komponen yang baru memanggil import itu waktu pertama kali dirender. FileHeavyChart.jsxharus punyaexport default.lazydipanggil di luar komponen, sekali saja, sama sepertiimportbiasa.<Suspense fallback={...}>membungkus komponen lazy. Selama chunk-nya belum datang, yang tampil fallback-nya. SatuSuspenseboleh membungkus beberapa komponen lazy sekaligus.fallbackboleh elemen apa saja: teks seperti<div id="loading">Loading...</div>, spinner, atau kerangka abu-abu seukuran kontennya supaya halaman gak melompat.
| Waktu | Yang terjadi | Yang tampil |
|---|---|---|
| 0 detik | Tombol diklik, HeavyChart dirender, chunk mulai diunduh |
Sedang memuat... |
| 0,9 detik | Chunk 180 KB selesai | Komponen Berhasil Dimuat! dan grafiknya |
Kedua kalinya langsung tampil
lazy mengingat hasil import-nya. Setelah chunk pertama kali selesai, HeavyChart yang disembunyikan lalu ditampilkan lagi langsung muncul, tanpa fallback dan tanpa unduhan baru. Itu jawaban tebakannya: fallback-nya gak muncul lagi. Browser juga menyimpan file chunk-nya di cache, jadi kunjungan berikutnya pun lebih cepat.
Mensimulasikan dengan state
Editor latihan gak punya file lain untuk di-import. Di latihan pertama, import-nya ditiru dengan promise yang selesai 300 ms kemudian dengan { default: Grafik }, jadi lazy dan Suspense sungguhan menampilkan fallback-nya. Di latihan kedua, kamu meniru tiga keadaannya dengan state biasa: belum dimuat, sedang memuat, sudah dimuat.
function App() {
const [loaded, setLoaded] = useState(false)
const [loading, setLoading] = useState(false)
const handleLoad = () => {
setLoading(true)
setLoaded(true)
setLoading(false)
}
return (
<div>
<button id="load" onClick={handleLoad}>Muat Komponen</button>
{!loaded && !loading && <p id="placeholder">Klik tombol untuk memuat</p>}
{loading && <p id="fallback">Sedang memuat...</p>}
{loaded && <div id="lazy-content">Komponen Berhasil Dimuat!</div>}
</div>
)
}
- Sebelum diklik,
loadeddanloadingsama-samafalse, jadi cuma#placeholderyang tampil. - Ketiga setter di
handleLoaddijalankan React dalam satu render, jadi#fallbackdi sini gak sempat terlihat. Yang sungguh menampilkannya selama menunggu adalahSuspensedenganlazy. - Setelah diklik,
#lazy-contenttampil dan#placeholderhilang.
⚠️ Jebakan yang sering ketemu
lazydi dalam komponen.const Chart = lazy(...)di dalamAppmembuat komponen baru tiap render: fallback muncul lagi dan state di dalamnya hilang. Tulislazydi luar komponen.- Named export.
lazybutuhexport default. Untukexport function Chart, tulislazy(() => import("./Chart").then((m) => ({ default: m.Chart }))). - Semua komponen di-lazy. Tiap chunk adalah satu request tambahan. Lazy cocok untuk yang besar dan jarang dibuka: halaman lain, modal, editor, grafik. Yang langsung terlihat di atas halaman jangan di-lazy.
- Placeholder yang tertinggal. Setelah tombol diklik, ajakan "Klik tombol untuk memuat" harus hilang, bukan tetap tampil di samping kontennya.
TL;DR: lazy(() => import("./HeavyChart")) memisahkan komponen ke chunk sendiri yang diunduh waktu pertama kali dirender, dan <Suspense fallback={...}> menampilkan fallback selama chunk-nya di jalan. Hasilnya bundle awal lebih kecil dan halaman lebih cepat tampil: 300 KB jadi 120 KB di contoh tadi. lazy ditulis di luar komponen dan butuh export default. Setelah dimuat sekali, komponennya langsung tampil tanpa fallback. Di latihan pertama, import-nya ditiru dengan promise, dan di latihan kedua, tiga keadaannya (placeholder, #fallback, #lazy-content) ditiru dengan state.