Code splitting artinya memecah JavaScript aplikasimu jadi beberapa file kecil (disebut chunk), lalu browser cuma mengunduh chunk yang dibutuhkan halaman yang sedang dibuka. Sisanya diunduh nanti, saat benar-benar dipakai. Mirip beli ensiklopedia 20 jilid: toko yang pintar mengirim jilid yang mau kamu baca sekarang dulu, jilid lain dikirim waktu kamu memintanya. Kamu bisa mulai baca jauh lebih cepat.
🧪 Tebakan cepat: toko online di Lab punya JavaScript 510 KB. Kalau halaman Produk dan Admin (beserta library grafiknya) dipisah jadi chunk sendiri, berapa KB yang masih diunduh saat user membuka Beranda: 510 KB, 220 KB, atau 30 KB? Tebak dulu, jawabannya ada di bagian "Split per route dengan React.lazy".
Bundle dan chunk
Waktu kamu menjalankan npm run build, bundler (Vite, webpack, Rollup, esbuild) mengumpulkan semua file JavaScript-mu dan library yang kamu pakai, lalu menggabungkannya jadi file siap kirim. Hasil gabungan itu disebut bundle. Tanpa code splitting, semuanya masuk satu bundle, termasuk halaman yang gak pernah dibuka user.
Ini toko online di Lab:
| Bagian | Ukuran | Dipakai di |
|---|---|---|
| React, React DOM, router | 150 KB | semua halaman |
| App dan layout (header, menu) | 40 KB | semua halaman |
Beranda.jsx |
30 KB | / |
Produk.jsx |
50 KB | /produk |
Admin.jsx |
60 KB | /admin |
GrafikPenjualan.jsx dan library grafiknya |
180 KB | /admin, setelah tombol Lihat grafik diklik |
Total 510 KB. Anggap jaringannya 1,6 Mbps, kira-kira 200 KB per detik: satu bundle utuh butuh sekitar 2,55 detik cuma untuk diunduh. Padahal user yang membuka Beranda cuma butuh 220 KB. Setelah diunduh pun, tiap KB JavaScript masih harus di-parse dan dijalankan, jadi kode yang gak dipakai tetap memperlambat.
Chunk adalah potongan bundle yang jadi file sendiri. Kamu gak memotongnya manual: kamu cukup menandai "bagian ini boleh dimuat nanti", lalu bundler membuatkan chunk-nya.
import() memuat module saat dibutuhkan
Penandanya adalah dynamic import, import(). Bedanya dengan import biasa:
// Import statis: ditulis di paling atas file, langsung ikut bundle utama
import { formatRupiah } from './utils.js';
// Dynamic import: dipanggil seperti function, kapan saja
tombolGrafik.addEventListener('click', async () => {
const { gambarGrafik } = await import('./grafik.js'); // baru diunduh sekarang
gambarGrafik(dataPenjualan);
});
import ... from(statis) cuma boleh di level paling atas file. Module-nya pasti ikut di-load bersama file itu.import('./grafik.js')dipanggil seperti function dan mengembalikan Promise. Promise itu selesai dengan isi module-nya: export bernama jadi property (gambarGrafik), export default ada di.default.- Karena mengembalikan Promise, pakai
await(atau.then). Selama menunggu, file-nya sedang diunduh. - Tiap
import()jadi tanda untuk bundler:grafik.jsdan semua yang cuma dia pakai dipisah jadi chunk sendiri.
Jadi sintaks yang memuat module saat runtime adalah import().
Split per route dengan React.lazy
Cara paling umum: tiap halaman (route) jadi chunk sendiri. Di React, pakai lazy dan Suspense:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router';
import Beranda from './pages/Beranda.jsx'; // halaman pertama, tetap statis
// Jadi chunk sendiri, baru diunduh saat route-nya dibuka
const Produk = lazy(() => import('./pages/Produk.jsx'));
const Admin = lazy(() => import('./pages/Admin.jsx'));
export default function App() {
return (
<Suspense fallback={<p>Memuat halaman...</p>}>
<Routes>
<Route path="/" element={<Beranda />} />
<Route path="/produk" element={<Produk />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</Suspense>
);
}
lazy(() => import(...))membuat komponen yang kodenya baru diunduh saat pertama kali di-render. File-nya harus punyaexport defaultberisi komponen.<Suspense fallback={...}>menampilkan fallback selama chunk-nya diunduh. Pasang dia di atas komponen lazy, supaya selama menunggu ada yang bisa ditampilkan.Berandatetap import statis, karena hampir semua user membukanya duluan.
Hasilnya, unduhan awal tinggal React dan router (150 KB), layout (40 KB), dan Beranda (30 KB): 220 KB, dari 510 KB. Itu jawaban tebakannya. Waktu unduhnya turun dari sekitar 2,55 detik ke 1,1 detik.
Split per komponen: muat saat diklik
Halaman Admin punya grafik penjualan dengan library 180 KB, padahal grafiknya baru muncul setelah tombol diklik. Pisahkan juga:
import { lazy, Suspense, useState } from 'react';
const GrafikPenjualan = lazy(() => import('./GrafikPenjualan.jsx'));
export default function Admin() {
const [tampil, setTampil] = useState(false);
return (
<main>
<h1>Admin</h1>
<button onClick={() => setTampil(true)}>Lihat grafik</button>
{tampil && (
<Suspense fallback={<p>Memuat grafik...</p>}>
<GrafikPenjualan />
</Suspense>
)}
</main>
);
}
Selama tampil masih false, GrafikPenjualan gak pernah di-render, jadi chunk-nya gak pernah diunduh. Pindah ke Admin sekarang cuma mengunduh 60 KB. Library grafik 180 KB menunggu sampai tombolnya diklik.
Harga yang dibayar: menunggu saat pindah
Code splitting gak menghapus kode, cuma memindahkan waktu unduhnya. Kalau Admin dan grafiknya satu chunk, user yang pindah ke Admin menunggu 240 KB, sekitar 1,2 detik, ditambah satu kali bolak-balik request ke server. Selama itu yang terlihat cuma fallback.
Karena itu, yang dipecah adalah bagian yang besar dan jarang dibutuhkan di awal: halaman lain, editor, grafik, peta. Bagian yang dipakai semua halaman (layout, tombol login) tetap di bundle utama. Memecah terlalu kecil juga rugi: tiap chunk satu request lagi. Untuk menghilangkan jeda saat pindah, browser bisa diminta mengunduh chunk halaman berikutnya diam-diam lebih dulu, dibahas di pelajaran resource hints.
💡 Tree shaking butuh ES modules
Selain dipecah, bundle bisa dikecilkan dengan tree shaking: bundler membuang export yang gak pernah di-import. Kalau utils.js punya 20 function dan kamu cuma memakai formatRupiah, sisanya gak ikut ke bundle.
Syaratnya, kode itu ditulis sebagai ES module (import dan export), karena bundler bisa membaca dari teksnya saja apa yang dipakai. CommonJS (require dan module.exports) umumnya gak bisa di-tree-shake. Contoh yang sering ketemu: lodash (CommonJS) ikut utuh walaupun kamu cuma butuh debounce, sedangkan lodash-es (versi ES module) cuma menyertakan yang kamu import.
🎯 Cari yang bikin bundle gemuk
Sebelum memecah, cari tahu dulu apa yang besar. vite build sudah mencetak ukuran tiap chunk. Untuk melihat isinya per module, pakai rollup-plugin-visualizer (Vite dan Rollup) atau webpack-bundle-analyzer (webpack): keduanya menggambar kotak per module, makin besar kotaknya makin besar module-nya. Library besar yang cuma dipakai satu halaman adalah calon chunk pertama.
⚠️ Jebakan yang sering ketemu
importstatis di dalamifatau function.if (isAdmin) { import Grafik from './Grafik.jsx' }adalah SyntaxError, file-nya gak jalan sama sekali. Untuk memuat belakangan, pakaiimport().- Komponen lazy tanpa
Suspense. React butuh<Suspense>di atasnya untuk tahu apa yang ditampilkan selama chunk diunduh. - Lupa
export default.lazy()mengambil export default. Kalau file-nya cuma punya export bernama, isinyaundefineddan React melempar error. - Memecah yang dipakai semua halaman. Layout atau header yang di-lazy cuma menambah satu request dan jeda di awal, tanpa menghemat apa pun.
TL;DR: Code splitting memecah bundle JavaScript jadi chunk yang dimuat sesuai kebutuhan. Tandanya import(): dipanggil kapan saja, mengembalikan Promise, dan bundler menjadikan module-nya chunk sendiri. Di React, lazy(() => import('./Admin.jsx')) di dalam <Suspense> membuat tiap route jadi chunk; di Lab unduhan awal turun dari 510 KB ke 220 KB. Komponen berat yang muncul setelah diklik (grafik 180 KB) dipisah juga. Harganya: user menunggu saat pindah, jadi pecah yang besar dan jarang dipakai di awal, bukan layout. Tree shaking membuang export yang gak dipakai, tapi cuma andal di ES modules.