Aplikasi toko Budi terus tumbuh: ada beranda, laporan penjualan, galeri produk, sampai panel pengaturan. Semua kodenya tergabung dalam satu berkas besar yang diimpor statis — dan akibatnya setiap pengunjung yang hanya ingin melihat beranda tetap mengunduh kode panel pengaturan yang tidak pernah dibukanya.

Code splitting menjawab masalah ini: alih-alih satu bundle tunggal, kode dipecah menjadi beberapa berkas yang dimuat sesuai kebutuhan. Chapter ini membandingkan kedua bentuknya dengan module yang sama, lalu membahas pola pemuatan per rute yang menjadi dasar framework aplikasi web modern.

Satu istilah perlu disepakati lebih dulu. Bundle adalah berkas gabungan hasil build tool — banyak berkas sumber dirapatkan menjadi satu (atau beberapa) berkas yang siap dikirim ke browser. Konsepnya mirip pembersihan ukuran pada chapter sebelumnya, tetapi tujuannya bukan memperkecil teks, melainkan menata unit pemuatan: berapa banyak berkas, dan apa isi masing-masing.

Supaya proses pemuatan bisa diamati, ketiga module contoh di bawah sengaja diberi satu console.log di tingkat atas. Baris itu hanya berjalan ketika module benar-benar dievaluasi — jadi setiap log dimuat yang muncul di keluaran adalah bukti bahwa module itu dimuat pada momen tersebut.

Bundle Tunggal: Semua Orang Mengunduh Semua #

Mekanismenya ada di sifat impor statis yang sudah kita pelajari di section Modules: seluruh import di tingkat atas diselesaikan lebih dulu, membentuk module graph yang diunduh dan dievaluasi dari akar sampai daun, sebelum baris pertama badan program berjalan. Tidak ada jalan tengah — begitu satu module tercantum sebagai impor statis, module itu ikut termuat setiap saat.

Kita buktikan dengan tiga module halaman yang masing-masing mencatat kapan ia dimuat:

// rute-beranda.mjs — module halaman beranda
console.log("module rute-beranda dimuat");

export function render() {
  return "Selamat datang di toko Budi";
}
// rute-laporan.mjs — module halaman laporan
console.log("module rute-laporan dimuat");

export function render() {
  return "Laporan penjualan minggu ini";
}
// rute-galeri.mjs — module halaman galeri
console.log("module rute-galeri dimuat");

export function render() {
  return "Galeri produk musim ini";
}

Berkas pertama menggabungkan ketiganya dengan impor statis — pola bundle tunggal:

// gabung-statis.mjs — semua halaman diimpor statis (pola bundle tunggal)
import { render as renderBeranda } from "./rute-beranda.mjs";
import { render as renderLaporan } from "./rute-laporan.mjs";
import { render as renderGaleri } from "./rute-galeri.mjs";

console.log(renderBeranda());
console.log(renderLaporan());
console.log(renderGaleri());

Menjalankan node gabung-statis.mjs menghasilkan:

Output yang Dihasilkan:

text
module rute-beranda dimuat
module rute-laporan dimuat
module rute-galeri dimuat
Selamat datang di toko Budi
Laporan penjualan minggu ini
Galeri produk musim ini

Mari kita telaah baris-baris pentingnya:

  1. Baris 2–4 (import ... from ...): ketiga jalur dicatat oleh module graph sebelum program berjalan. Urutan keluaran membuktikannya — ketiga baris log dimuat muncul lebih dulu, sebelum satu pun console.log pada badan program dieksekusi.
  2. Baris 6–8 (console.log(render...())): pemanggilan render hanya mencetak teks halaman. Perhatikan bahwa rute-galeri tetap dimuat meskipun pada skenario nyata galeri mungkin tidak pernah dibuka pengunjung — biaya pemuatannya sudah dibayar sejak awal.

Log “module rute-galeri dimuat” pada berkas gabungan itulah biaya yang ingin dihapus: satu pengunjung, satu halaman, tetapi tiga module terunduh dan tereksekusi.

Pada contoh kecil ini selisihnya tampak sepele — tiga module pendek. Bayangkan penerapannya pada aplikasi nyata: tiga halaman masing-masing berisi ratusan baris kode dan satu pustaka gambar grafik yang gemuk. Dengan bundle tunggal, pustaka grafik itu terunduh juga untuk pengunjung yang hanya membaca beranda — dan hal itu berulang untuk setiap halaman lain yang tidak dibuka.

Bundle Terpecah: Dimuat per Rute #

Versi terpecah menahan keputusan pemuatan sampai rute benar-benar dibuka. Kuncinya adalah import() dinamis yang kita pelajari di chapter Dynamic Import — di sini dipakai sebagai alat performa:

// aplikasi-rute.mjs — satu module per rute, dimuat saat rute dibuka
const rute = {
  "/beranda": () => import("./rute-beranda.mjs"),
  "/laporan": () => import("./rute-laporan.mjs"),
  "/galeri": () => import("./rute-galeri.mjs"),
};

async function navigasi(jalur) {
  const muat = rute[jalur];
  if (!muat) {
    console.log("rute tidak dikenal:", jalur);
    return;
  }
  const halaman = await muat();
  console.log(halaman.render());
}

await navigasi("/beranda");
await navigasi("/laporan");
await navigasi("/beranda"); // dibuka kedua kali: module sudah di cache
await navigasi("/profil");

Menjalankan node aplikasi-rute.mjs menghasilkan:

Output yang Dihasilkan:

text
module rute-beranda dimuat
Selamat datang di toko Budi
module rute-laporan dimuat
Laporan penjualan minggu ini
Selamat datang di toko Budi
rute tidak dikenal: /profil

Mari kita telaah baris-baris pentingnya:

  1. Baris 3–7 (object rute): setiap nilai adalah function panah yang hanya didefinisikan, belum dipanggil. Karena import() di dalamnya belum dieksekusi, tidak ada satu pun module halaman yang dimuat saat object ini dibuat — berbeda dengan impor statis yang sudah mengikat sejak awal.
  2. Baris 10 (const muat = rute[jalur]): jalur yang diminta dipakai sebagai kunci. Pemilihan module berpindah dari waktu tulis (impor statis) ke waktu jalan (kunci object).
  3. Baris 11–14 (if (!muat)): rute yang tidak terdaftar ditanggapi dengan pesan dan return, bukan dengan pemanggilan import() yang tidak pernah dibutuhkan.
  4. Baris 15 (const halaman = await muat()): pemanggilan function memicu import() saat itu juga, await menunggu module siap, dan hasilnya adalah objek namespace yang dipakai baris berikutnya.
  5. Baris 19–22 (empat navigasi): keluarannya membuktikan tiga perilaku sekaligus. /beranda memuat module-nya; /laporan memuat module-nya sendiri; /beranda kedua kali tidak mencetak log dimuat lagi karena module sudah ada di cache; /profil ditolak tanpa memuat apa pun. rute-galeri tidak pernah dimuat sama sekali dalam seluruh sesi ini.

Pola ini juga menjawab pertanyaan lamanya: bundle tunggal mengunduh semua kode untuk semua orang, sedangkan bundle terpecah mengunduh kode untuk halaman yang benar-benar dibuka.

Pola Route-Based di Aplikasi Web #

Pola di atas disebut route-based code splitting dan menjadi standar aplikasi web modern: satu bundle kecil berisi kerangka aplikasi, ditambah potongan kode (chunk) terpisah untuk tiap halaman yang diunduh saat navigasi. Build tool seperti yang dibahas di section Tooling dan Code Quality mengenali setiap import() di kode dan otomatis memisahkannya menjadi berkas hasil build tersendiri — kita cukup menulis import(), tooling mengurus pemecahannya. Router pada framework seperti Next.js menerapkan pola yang sama secara otomatis per halaman.

Alur navigasinya bisa diringkas empat langkah:

  1. Pengguna membuka aplikasi: hanya kerangka + rute aktif yang diunduh.
  2. Pengguna menekan tautan ke halaman lain: router memanggil import() untuk module halaman itu.
  3. Browser mengunduh chunk-nya, module dievaluasi sekali, lalu halaman dirender.
  4. Kunjungan ulang ke rute yang sama: cache module menjawab tanpa unduhan baru — persis perilaku /beranda kedua pada keluaran di atas.

Aturan praktis pembagiannya bisa dirangkum dua kalimat: kode yang selalu dipakai di jalur utama (kerangka halaman, utilitas inti) diimpor statis agar ikut dimuat dari awal; kode yang hanya dipakai di jalur tertentu (halaman sekunder, panel besar, fitur opsional) dimuat dinamis.

Bundle per rute sudah menurunkan biaya pemuatan secara besar. Tingkat berikutnya lebih halus lagi: memuat sebuah module bukan saat halaman dibuka, melainkan saat interaksi tertentu terjadi — itulah lazy loading yang dibahas chapter berikutnya.