Bab 5: Document & Resource Loading

4 menit baca

Kapan halaman siap? Bagaimana cara memuat script dengan benar? Apa yang terjadi saat gambar gagal dimuat?


5.1 Page Lifecycle: DOMContentLoaded, load, beforeunload, unload

💡Analogi

Memuat halaman web itu kayak membangun rumah:

  1. DOMContentLoaded = Struktur rumah selesai (dinding, atap) — belum ada furniture
  2. load = Rumah selesai total (furniture, dekorasi, semua terpasang)
  3. beforeunload = Penghuni mau pindah — "Yakin mau pergi?"
  4. unload = Penghuni sudah pergi, rumah kosong

DOMContentLoaded

Fire saat HTML sudah selesai di-parse dan DOM tree sudah siap. Gambar dan stylesheet mungkin belum selesai dimuat.

javascript
document.addEventListener("DOMContentLoaded", function() {
  // DOM sudah siap! Bisa manipulasi elemen
  console.log("DOM ready!");
  
  const btn = document.getElementById("my-btn"); // ✅ Pasti ada
  btn.addEventListener("click", () => alert("Halo!"));
});

Kenapa penting? Karena script di <head> jalan SEBELUM body di-parse. Tanpa DOMContentLoaded, elemen belum ada:

html
<head>
  <script>
    // ❌ GAGAL — body belum ada!
    document.body.style.background = "red";
    
    // ✅ Tunggu DOM ready
    document.addEventListener("DOMContentLoaded", () => {
      document.body.style.background = "red";
    });
  </script>
</head>

DOMContentLoaded dan Script

Script biasa MEMBLOKIR DOMContentLoaded:

html
<script>
  // DOMContentLoaded MENUNGGU script ini selesai
  // Termasuk kalau script ini fetch data dari server
</script>

Kecuali script dengan async atau defer (dibahas di sub-bab berikutnya).

DOMContentLoaded dan Stylesheet

Stylesheet sendiri tidak memblokir DOMContentLoaded. TAPI kalau ada script SETELAH stylesheet, script menunggu stylesheet → DOMContentLoaded menunggu script → efeknya stylesheet memblokir.

html
<link rel="stylesheet" href="style.css">
<script>
  // Script ini menunggu style.css selesai dimuat
  // DOMContentLoaded menunggu script ini
  // Jadi DOMContentLoaded juga menunggu style.css
</script>

window.onload

Fire saat SEMUA resource selesai dimuat (gambar, stylesheet, iframe, dll):

javascript
window.addEventListener("load", function() {
  // Semua sudah dimuat — gambar sudah tampil, CSS sudah diterapkan
  console.log("Halaman selesai total!");
  
  // Bisa baca ukuran gambar yang sebenarnya
  const img = document.querySelector("img");
  console.log(img.naturalWidth, img.naturalHeight);
});

beforeunload — Konfirmasi Sebelum Pergi

javascript
window.addEventListener("beforeunload", function(event) {
  // Tampilkan dialog konfirmasi (teks tidak bisa di-custom di browser modern)
  event.preventDefault();
  event.returnValue = ""; // Required untuk beberapa browser
});

// Biasanya dipasang HANYA kalau ada perubahan yang belum disimpan:
let hasUnsavedChanges = false;

input.addEventListener("input", () => { hasUnsavedChanges = true; });

window.addEventListener("beforeunload", function(event) {
  if (hasUnsavedChanges) {
    event.preventDefault();
    event.returnValue = "";
  }
});

unload — Halaman Ditinggalkan

javascript
window.addEventListener("unload", function() {
  // Halaman sedang ditutup/navigasi
  // Waktu sangat terbatas! Hanya untuk cleanup ringan
  
  // Kirim analytics (pakai navigator.sendBeacon, bukan fetch)
  navigator.sendBeacon("/analytics", JSON.stringify({
    page: location.href,
    timeSpent: Date.now() - startTime
  }));
});

document.readyState

Cek status loading saat ini:

javascript
console.log(document.readyState);
// "loading" — masih memuat HTML
// "interactive" — HTML selesai, resource masih loading (= DOMContentLoaded)
// "complete" — semua selesai (= load)

// Event saat state berubah
document.addEventListener("readystatechange", function() {
  console.log("State:", document.readyState);
});

// Pattern: jalankan kode kalau DOM sudah ready ATAU tunggu
if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init(); // DOM sudah ready
}
⚠️Jebakan!
javascript
// 1. DOMContentLoaded di document, BUKAN window
document.addEventListener("DOMContentLoaded", handler); // ✅
// window.addEventListener("DOMContentLoaded", handler); // Juga bekerja, tapi konvensi di document

// 2. Kalau script di akhir <body>, DOMContentLoaded hampir tidak perlu
// Karena semua elemen sudah ada saat script jalan

// 3. beforeunload: browser modern TIDAK menampilkan custom message
// Hanya dialog generic "Leave site?"

// 4. unload: JANGAN pakai fetch/XMLHttpRequest di sini
// Pakai navigator.sendBeacon() — dijamin terkirim

// 5. DOMContentLoaded TIDAK menunggu gambar
// Kalau perlu ukuran gambar → pakai window.onload atau img.onload

🎯 Challenge

javascript
// Buat loading screen:
// 1. Tampilkan <div id="loader"> (overlay full screen, "Loading...")
// 2. Saat DOMContentLoaded → log "DOM ready" (loader masih tampil)
// 3. Saat window load → sembunyikan loader dengan animasi fade-out
// 4. Tambahkan beberapa <img> besar untuk melihat perbedaan timing
// 5. Tampilkan waktu antara DOMContentLoaded dan load

5.2 Script: async dan defer

💡Analogi

Bayangin kamu lagi baca buku (HTML parsing). Ada 3 cara handle "tugas tambahan" (script):

  • Biasa (tanpa atribut) = Berhenti baca, kerjakan tugas, lanjut baca
  • defer = Catat tugas, lanjut baca, kerjakan semua tugas setelah selesai baca (berurutan)
  • async = Suruh orang lain download tugas, lanjut baca, kerjakan tugas begitu siap (tidak berurutan)

Script Biasa (Blocking)

html
<head>
  <script src="big-library.js"></script>
  <!-- ❌ Browser BERHENTI parse HTML sampai script selesai download + execute -->
  <!-- User melihat halaman kosong lebih lama -->
</head>

defer — Download Paralel, Execute Setelah Parse

html
<head>
  <script defer src="library.js"></script>
  <script defer src="app.js"></script>
  <!-- ✅ Download paralel dengan HTML parsing -->
  <!-- Execute SETELAH HTML selesai di-parse, SEBELUM DOMContentLoaded -->
  <!-- Urutan DIJAMIN: library.js dulu, baru app.js -->
</head>

Karakteristik defer:

  • Download paralel (tidak blocking)
  • Execute setelah DOM ready
  • Urutan terjaga (sesuai urutan di HTML)
  • DOMContentLoaded menunggu defer scripts selesai

async — Download Paralel, Execute Segera

html
<head>
  <script async src="analytics.js"></script>
  <script async src="ads.js"></script>
  <!-- ✅ Download paralel -->
  <!-- Execute SEGERA setelah download selesai -->
  <!-- ⚠️ Urutan TIDAK dijamin! Yang selesai duluan, jalan duluan -->
</head>

Karakteristik async:

  • Download paralel (tidak blocking)
  • Execute segera setelah download selesai
  • Urutan TIDAK dijamin
  • DOMContentLoaded TIDAK menunggu async scripts
  • Bisa jalan sebelum atau sesudah DOM ready

Perbandingan Visual

HTML: ████████████████████████████████████████ Biasa: ████──download──██execute██──████████ (parsing berhenti) defer: ████████████████████████████████──execute── ──download──(paralel) (setelah parse) async: ████████████──execute──█████████████ ──download── (segera setelah download)

Kapan Pakai Yang Mana?

SituasiGunakan
Script butuh DOM (app utama)defer
Script independen (analytics, ads)async
Script kecil inlineTaruh di akhir <body>
Script bergantung pada script laindefer (urutan terjaga)

Dynamic Script

javascript
// Membuat script secara dinamis
const script = document.createElement("script");
script.src = "module.js";

// Dynamic script = async by default!
document.body.append(script); // Mulai download, execute segera setelah ready

// Kalau mau urutan terjaga:
script.async = false; // Jadi seperti defer
document.body.append(script);

Pattern: Load Script On-Demand

javascript
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.append(script);
  });
}

// Pakai
async function init() {
  await loadScript("https://cdn.example.com/chart.js");
  // chart.js sudah siap, bisa pakai
  new Chart(...);
}
⚠️Jebakan!
javascript
// 1. defer dan async HANYA untuk script EXTERNAL (dengan src)
// <script defer>console.log("ini")</script> → defer DIABAIKAN!

// 2. async script bisa jalan SEBELUM DOM ready
// Jangan akses DOM di async script tanpa cek readyState

// 3. defer script jalan SEBELUM DOMContentLoaded
// Tapi SETELAH HTML selesai di-parse

// 4. Urutan async TIDAK dijamin
// Jangan pakai async kalau script B bergantung pada script A

// 5. type="module" otomatis defer
// <script type="module" src="app.js"></script> → defer by default

🎯 Challenge

html
<!-- Buat halaman dengan 3 script:
1. analytics.js (async) — log "Analytics loaded" + timestamp
2. library.js (defer) — log "Library loaded" + timestamp  
3. app.js (defer) — log "App loaded" + timestamp

Pertanyaan:
- Urutan mana yang dijamin?
- Apakah DOMContentLoaded menunggu semua?
- Apa yang terjadi kalau analytics.js sangat besar?

Buat file HTML dan test di browser, perhatikan urutan console.log
-->

5.3 Resource Loading: onload dan onerror

💡Analogi

Saat browser memuat resource (gambar, script, stylesheet), hasilnya cuma dua: berhasil atau gagal. Kayak pesan delivery — kamu perlu tahu apakah paket sampai atau hilang di jalan.

Script: onload dan onerror

javascript
const script = document.createElement("script");
script.src = "https://cdn.example.com/library.js";

script.onload = function() {
  // Script berhasil dimuat DAN dieksekusi
  console.log("Library siap!");
  // Sekarang bisa pakai fungsi dari library
};

script.onerror = function() {
  // Gagal memuat (404, network error, CORS block)
  console.error("Gagal memuat script!");
};

document.head.append(script);

Image: onload dan onerror

javascript
const img = document.createElement("img");

img.onload = function() {
  console.log(`Gambar dimuat: ${img.naturalWidth}x${img.naturalHeight}`);
  document.body.append(img);
};

img.onerror = function() {
  console.error("Gambar gagal dimuat!");
  // Tampilkan placeholder
  img.src = "placeholder.png";
};

img.src = "photo.jpg"; // Mulai loading SETELAH set src

Preload Gambar

javascript
// Preload gambar sebelum ditampilkan
function preloadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Gagal load: ${src}`));
    img.src = src;
  });
}

// Pakai
async function showGallery(urls) {
  const images = await Promise.all(urls.map(preloadImage));
  images.forEach(img => document.body.append(img));
}

Stylesheet: onload dan onerror

javascript
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "theme-dark.css";

link.onload = function() {
  console.log("Stylesheet dimuat!");
};

link.onerror = function() {
  console.error("Stylesheet gagal!");
};

document.head.append(link);

Cross-Origin (CORS)

Script dari domain lain yang error → browser TIDAK kasih detail error (security):

javascript
// Script dari CDN lain
const script = document.createElement("script");
script.src = "https://other-domain.com/script.js";

// Untuk dapat detail error, tambahkan crossorigin
script.crossOrigin = "anonymous";

// Dan server harus kirim header: Access-Control-Allow-Origin

Global Error Handler: window.onerror

javascript
window.onerror = function(message, source, lineno, colno, error) {
  console.error("Global error:", {
    message,  // Pesan error
    source,   // URL file
    lineno,   // Baris
    colno,    // Kolom
    error     // Error object
  });
  
  // Kirim ke error tracking service
  sendToErrorTracker({ message, source, lineno, colno });
  
  // return true → cegah error muncul di console
  // return false/undefined → error tetap muncul di console
};
javascript
const gallery = document.getElementById("gallery");
const imageUrls = [
  "photo1.jpg",
  "photo2.jpg",
  "broken-link.jpg", // Ini akan gagal
  "photo3.jpg"
];

imageUrls.forEach(url => {
  const img = document.createElement("img");
  img.className = "gallery-img loading";
  
  img.onload = function() {
    img.classList.remove("loading");
    img.classList.add("loaded");
  };
  
  img.onerror = function() {
    img.classList.remove("loading");
    img.classList.add("error");
    img.src = "placeholder.svg"; // Fallback
    img.alt = "Gambar tidak tersedia";
  };
  
  img.src = url;
  gallery.append(img);
});
⚠️Jebakan!
javascript
// 1. onload/onerror harus di-set SEBELUM src (untuk img)
// Kalau img sudah di-cache, onload bisa fire synchronously
const img = new Image();
img.onload = handler;  // ✅ Set dulu
img.src = "photo.jpg"; // Baru set src

// 2. onerror pada script: TIDAK dapat detail error dari cross-origin
// Hanya "Script error." tanpa info berguna
// Solusi: crossOrigin attribute + CORS header di server

// 3. CSS @import di dalam stylesheet TIDAK trigger onload/onerror terpisah

// 4. iframe juga punya onload (fire saat halaman di dalam iframe selesai)

// 5. Gambar dari cache: onload tetap fire (tapi sangat cepat)

🎯 Challenge

javascript
// Buat image loader dengan progress:
// 1. Array 5 URL gambar (campur yang valid dan invalid)
// 2. Tampilkan progress: "Loading 2/5..."
// 3. Gambar berhasil → tampilkan di halaman
// 4. Gambar gagal → tampilkan placeholder dengan teks "Error"
// 5. Setelah semua selesai (berhasil atau gagal) → tampilkan summary:
//    "Berhasil: 3, Gagal: 2"
// Hint: Promise.allSettled(), img.onload, img.onerror

Ringkasan Bab 5

Sub-babInti
Page LifecycleDOMContentLoaded (DOM ready) → load (semua ready) → beforeunload → unload
async/deferdefer = urutan terjaga, setelah parse. async = segera, tanpa urutan
Resource Loadingonload/onerror untuk script, img, link. Promise pattern untuk preload

Prinsip utama:

  • Taruh script di akhir <body> ATAU pakai defer
  • async hanya untuk script independen (analytics, ads)
  • Selalu handle onerror untuk resource external
  • navigator.sendBeacon() untuk kirim data saat unload
  • DOMContentLoaded = DOM siap, load = semua resource siap

Next: Bab 6 — Miscellaneous (Mutation Observer, Selection & Range, Event Loop)

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.