Halaman toko Budi terasa lambat saat pertama dibuka: layar kosong beberapa detik, lalu seluruh konten muncul sekaligus. Penyebabnya sering bukan kode yang berjalan lambat, melainkan urutan pemuatan — satu berkas JavaScript ditaruh di <head> tanpa atribut, dan browser menahan pembacaan seluruh HTML di bawahnya sampai berkas itu selesai diunduh dan dieksekusi.

Tahap pemuatan (loading) adalah pengungkit performa yang paling awal, karena terjadi sebelum satu baris pun dieksekusi. Tiga faktor yang menentukannya:

  • Waktu eksekusi: kapan script dijalankan relatif terhadap pemrosesan HTML.
  • Ukuran berkas: berapa banyak byte yang harus diunduh sebelum script siap.
  • Jumlah berkas: setiap berkas terpisah berarti permintaan jaringan tersendiri.

Chapter ini membahas ketiganya, sekaligus menyinggung kembali material defer dan async yang sudah kita pelajari di section Persiapan Lingkungan.

Script dan Parser HTML yang Menunggu #

Browser memproses HTML dari atas ke bawah sambil membangun DOM. Begitu parser bertemu <script src="..."> tanpa atribut, parser berhenti: berkas diunduh, lalu dieksekusi, dan hanya setelah kedua langkah itu selesai parser melanjutkan membaca sisa dokumen. Script inline di tengah dokumen juga menghentikan parser — singkatnya, eksekusi script selalu mendahului sisa pemrosesan HTML.

Konsekuensinya mudah dibayangkan: elemen apa pun yang ditulis di bawah script belum ada di DOM ketika script itu berjalan, dan pengguna melihat halaman yang setengah jadi lebih lama. Berkas berikut memperlihatkan penempatan yang menyebabkan hal itu terjadi:

Pada berkas halaman-blokir.txt, parser berhenti tepat di baris <script src="statistik.js"></script> di <head>. Elemen <h1> dan paragraf di <body> baru diproses setelah statistik.js selesai diunduh dan dieksekusi:

text
Parser HTML : <html> <head>  ⏸ terblokir di sini ⏸  <body> <h1> <p> selesai
statistik.js:            unduh ──────────  eksekusi ──

Garis waktu ini yang membuat halaman terasa kosong lebih lama daripada seharusnya — bukan karena script-nya lambat, tetapi karena semuanya menunggu script itu.

Refresher: Posisi, defer, dan async #

Ada tiga cara standar memulihkan parser, dan ketiganya sudah dibedah tuntas di chapter defer dan async. Tabel berikut merangkumnya sebagai pegangan:

PenempatanUnduhanEksekusiCocok untuk
Akhir <body> tanpa atributSetelah hampir semua HTML diprosesSegera setelah unduhanScript sederhana; halaman kecil
<script src="..." defer>Paralel dengan parsingSetelah dokumen selesai, urut sesuai tulisanScript utama yang butuh DOM lengkap
<script src="..." async>Paralel dengan parsingSegera setelah unduhan, urutan tidak dijaminScript pihak ketiga yang berdiri sendiri

Semua pilihan di atas memindahkan kapan script dieksekusi, bukan berapa besar yang harus diunduh. Untuk faktor kedua itu, kita perlu melihat isi berkasnya.

Ukuran Berkas yang Diunduh #

Aturannya sederhana: setiap byte yang kita tulis di berkas JavaScript berarti byte yang diunduh pengunjung lewat jaringan. Komentar, indentasi, dan baris kosong memang membantu manusia membaca kode, tetapi bagi runtime semuanya tidak berpengaruh. Praktik industri menyelesaikannya lewat minification: berkas dibersihkan dari segala yang tidak dibutuhkan runtime tanpa mengubah perilakunya.

Kita buktikan idenya dengan pembersihan sederhana — komentar, baris kosong, dan indentasi dilepas — lalu bandingkan ukuran keduanya:

// hitung-ukuran.js — membandingkan ukuran berkas sebelum dan sesudah diringkas
const sumber = `// statistik.js — versi bergaya penulisan normal
// Menghitung rata-rata penjualan harian toko.

function rataRata(daftarAngka) {
  let jumlah = 0;
  for (const angka of daftarAngka) {
    jumlah = jumlah + angka;
  }
  return jumlah / daftarAngka.length;
}

const penjualan = [120, 95, 140, 110];
console.log("Rata-rata:", rataRata(penjualan));
`;

// pembersihan sederhana: komentar, baris kosong, dan indentasi dilepas
const ringkas = sumber
  .split("\n")
  .filter((baris) => {
    const potong = baris.trim();
    return potong !== "" && !potong.startsWith("//");
  })
  .map((baris) => baris.trim())
  .join("\n");

console.log("versi biasa  :", Buffer.byteLength(sumber), "byte");
console.log("versi ringkas:", Buffer.byteLength(ringkas), "byte");
console.log(ringkas);

Menjalankan node hitung-ukuran.js menghasilkan:

Output yang Dihasilkan:

text
versi biasa  : 348 byte
versi ringkas: 235 byte
function rataRata(daftarAngka) {
let jumlah = 0;
for (const angka of daftarAngka) {
jumlah = jumlah + angka;
}
return jumlah / daftarAngka.length;
}
const penjualan = [120, 95, 140, 110];
console.log("Rata-rata:", rataRata(penjualan));

Kurang lebih sepertiga ukuran hilang, dan kode hasil pembersihan tetap bisa dijalankan karena hanya bagian yang tidak dibaca runtime yang dilepas. Mari kita telaah baris-baris pentingnya:

  1. Baris 2–15 (const sumber = \…``): template literal menampung versi “bergaya normal” dari sebuah berkas — lengkap dengan komentar, baris kosong, dan indentasi dua spasi. Inilah wujud yang nyaman bagi manusia.
  2. Baris 18–25 (split … join): rantai tiga tahap pembersihan. split("\n") mengubah teks menjadi array baris; filter membuang baris kosong dan baris yang diawali //; map dengan trim() melepas indentasi; join("\n") menyatukannya kembali menjadi satu teks.
  3. Baris 27–28 (Buffer.byteLength(...)): Buffer.byteLength menghitung jumlah byte dari sebuah string — inilah ukuran yang realistis untuk transfer, bukan length yang menghitung karakter. Selisih 348 ke 235 byte adalah hemat nyata untuk setiap pengunjung.
  4. Baris 29 (console.log(ringkas)): mencetak hasilnya untuk membuktikan bahwa kode tetap utuh secara sintaks — hanya rapinya yang hilang.

Pembersihan di atas sengaja dibuat sederhana agar terbaca. Minifier sungguhan seperti yang dipakai build tools (dibahas di section Tooling dan Code Quality) melangkah lebih jauh: mempersingkat nama variabel, menggabungkan ekspresi, dan menyisakan spasi hanya di tempat yang diwajibkan sintaks. Server modern menambah satu lapis lagi — kompresi gzip atau brotli yang mengecilkan byte yang benar-benar berpindah lewat jaringan.

Jumlah Berkas dan Permintaan Jaringan #

Faktor ketiga sering luput: setiap berkas terpisah adalah satu permintaan HTTP. Permintaan tidak hanya berisi byte kode — ia juga membawa bagian pembuka (header) dan biaya waktu menunggu jawaban server. Banyak berkas kecil berarti banyak pembukaan tersebut, dan total waktu muat bisa lebih lama daripada satu berkas yang berisi kode setara.

Browser modern meredam sebagian biayanya lewat koneksi yang mampu mengirim banyak permintaan sekaligus di satu jalur, tetapi keputusan praktisnya tetap sama: gabungkan yang selalu dipakai bersama, pecah yang sifatnya terpisah. Pecahan berlebihan di babak ini punya batas keringanan tersendiri — di sinilah code splitting pada chapter berikutnya menempatkan dirinya, memecah bukan demi banyaknya berkas, melainkan demi memuat yang dibutuhkan saja.

Satu penghematan tambahan datang dari sisi yang berbeda: cache browser. Pengunjung yang kembali membawa salinan berkas yang belum berubah, sehingga unduhan kedua bisa dilewati sama sekali. Ukuran berkas yang ramping tetap penting untuk kunjungan pertama; cache menjaga kunjungan-kunjungan berikutnya.

Tiga faktor pemuatan kini lengkap kita pegang: waktu eksekusi diatur dengan defer/async atau posisi, ukuran dikelola minifier dan kompresi, jumlah berkas dikelola dengan menggabungkan dan memecahnya dengan sengaja. Faktor ketiga itulah yang membuka pertanyaan menarik berikutnya: haruskah seluruh aplikasi memang berada dalam satu berkas besar? Chapter berikutnya menjawabnya dengan code splitting.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

hitung-ukuran.js

Unduh berkas
javascript
// hitung-ukuran.js — membandingkan ukuran berkas sebelum dan sesudah diringkas
const sumber = `// statistik.js — versi bergaya penulisan normal
// Menghitung rata-rata penjualan harian toko.

function rataRata(daftarAngka) {
  let jumlah = 0;
  for (const angka of daftarAngka) {
    jumlah = jumlah + angka;
  }
  return jumlah / daftarAngka.length;
}

const penjualan = [120, 95, 140, 110];
console.log("Rata-rata:", rataRata(penjualan));
`;

// pembersihan sederhana: komentar, baris kosong, dan indentasi dilepas
const ringkas = sumber
  .split("\n")
  .filter((baris) => {
    const potong = baris.trim();
    return potong !== "" && !potong.startsWith("//");
  })
  .map((baris) => baris.trim())
  .join("\n");

console.log("versi biasa  :", Buffer.byteLength(sumber), "byte");
console.log("versi ringkas:", Buffer.byteLength(ringkas), "byte");
console.log(ringkas);