Katalog toko Budi menampilkan seratus sampul buku. Memuat semuanya sekaligus berarti mengunduh seratus gambar — termasuk dua puluh yang berada jauh di bawah dan mungkin tidak pernah dilihat pengunjung. Cara lama menyelesaikannya memakai event scroll plus getBoundingClientRect() untuk memeriksa posisi tiap gambar: pendekatan itu berjalan puluhan kali per detik selama pengguliran, dan setiap pemeriksaan memaksa browser menghitung ulang tata letak. Hasilnya: halaman tersendat persis ketika pengunjung sedang bergerak.
IntersectionObserver membalik cara kerjanya. Alih-alih kode kita bertanya “apakah elemen sudah terlihat?” berulang-ulang, browser yang melaporkan: callback kita dipanggil secara asinkron setiap kali elemen masuk atau keluar dari area pandang (viewport). Perhitungan dilakukan browser di sisi mesin tata letaknya, tanpa membebani thread JavaScript — dan tanpa satu pun event scroll.
Nama API-nya memuat kata observer karena benar-benar sekelas dengan pengamat lain yang akan kita kenal di section ini: ResizeObserver untuk perubahan ukuran dan MutationObserver untuk perubahan DOM. Ketiganya berbagi pola yang sama — daftarkan target, sediakan callback, biarkan browser memberi kabar — sehingga pola yang dipelajari di sini terpakai berulang kali.
Membuat Observer dan Mendaftarkan Target #
Aturannya dua langkah: new IntersectionObserver(callback, opsi) membuat pengamat, lalu pengamat.observe(elemen) mendaftarkan elemen yang ingin dipantau. Callback menerima satu argumen: array entries, satu untuk setiap elemen yang status perpotongannya berubah — dengan properti isIntersecting yang menunjukkan apakah elemen sedang terlihat. Pola lazy loading gambar memakai atribut data-sumber sebagai penampung URL asli:
<!-- gambar ditandai tanpa src; URL asli disimpan di atribut data -->
<img data-sumber="/img/sampul-kamus.jpg" alt="Sampul Kamus Mini Bahasa">// lazy-load-gambar.js — memuat gambar saat mendekati viewport
// IntersectionObserver hanya ada di browser;
// berkas ini lolos node --check dan dijalankan pada halaman katalog.
const gambarBuku = document.querySelectorAll("img[data-sumber]");
const pengamat = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.sumber;
entry.target.removeAttribute("data-sumber");
pengamat.unobserve(entry.target); // cukup satu kali per gambar
console.log("Dimuat:", entry.target.alt);
}
}
},
{ rootMargin: "200px" } // mulai 200px sebelum gambar benar-benar terlihat
);
for (const img of gambarBuku) {
pengamat.observe(img);
}IntersectionObserver tidak tersedia di Node maupun jsdom, sehingga berkas ini diverifikasi sintaksnya dengan node --check lazy-load-gambar.js; keluaran berikut dicatat sebagai keluaran console browser sesuai perilaku yang dispesifikasikan standar. Ketika pengunjung menggulir katalog dan dua sampul pertama mendekati viewport, console mencatat:
Keluaran console browser:
Dimuat: Sampul Kamus Mini Bahasa
Dimuat: Sampul Cerita di Sekitar KitaMari kita telaah baris-baris pentingnya:
- Baris 5 (
querySelectorAll("img[data-sumber]")): semua gambar yang belum punyasrcdikumpulkan. Tanpasrc, browser tidak mengunduh apa pun — inilah yang menghemat seratus unduhan di awal. - Baris 7–19 (
new IntersectionObserver(...)): callback menerima array entries. Satu panggilan bisa memuat beberapa gambar sekaligus bila keduanya masuk viewport pada saat yang sama — itulah mengapa kita melakukan loop, bukan hanya membacaentries[0]. - Baris 10–14 (aksi saat terlihat):
entry.isIntersectingbernilaitrueberarti elemen memotong area pandang. Momen itu dipakai untuk memindahkandata-sumberkesrc— unduhan dimulai — lalu atribut penampung dibuang danunobservemelepas gambar dari pemantauan agar callback tidak dijalankan lagi untuk gambar yang sudah jadi. - Baris 18 (
rootMargin: "200px"): opsi ini memperluas area pandang secara virtual sejauh 200 pixel. Gambar mulai diunduh sebelum benar-benar terlihat, sehingga ketika sampulnya tiba di layar, gambarnya sudah siap. - Baris 21–23 (
pengamat.observe(img)): pendaftaran dilakukan untuk setiap gambar. Satu observer boleh melayani banyak target — membuat satu object per gambar justru boros.
Ada satu perilaku yang penting diketahui: setiap target yang didaftarkan langsung memicu callback satu kali dengan statusnya saat itu — terlihat atau tidak. Kita bisa memakai panggilan pertama ini untuk menandai gambar yang sudah berada di viewport sejak halaman dibuka, tanpa pemeriksaan khusus.
Cara Kerja di Balik Layar #
Memahami mekanismenya menjelaskan mengapa pendekatan ini hemat. Browser menghitung perpotongan setiap target dengan area pembanding sebagai bagian rutin dari langkah render — bukan sebagai reaksi atas event mouse atau keyboard. Ketika status perpotongan sebuah elemen melintasi ambang yang ditetapkan, browser mengantre callback kita sebagai tugas terpisah, membawa array entries berisi semua elemen yang statusnya berubah pada frame tersebut.
Perbandingan berikut merangkum perbedaannya dengan pola event scroll:
| Aspek | Event scroll + pemeriksaan manual | IntersectionObserver |
|---|---|---|
| Frekuensi | puluhan kali per detik selama pengguliran | hanya saat status perpotongan berubah |
| Biaya pemeriksaan | getBoundingClientRect() memaksa kalkulasi tata letak | dihitung browser pada langkah render yang memang berjalan |
| Kata berhenti | harus dijaga sendiri (throttle/debounce) | otomatis: tidak ada perubahan, tidak ada callback |
| Mengetahui “keluar viewport” | perlu logika tambahan | dikirim sebagai entry dengan isIntersecting: false |
Dua sifat entries perlu dibiasakan sejak sekarang. Pertama, entries datang berkelompok: satu panggilan callback bisa membawa beberapa elemen, termasuk elemen yang justru keluar viewport (isIntersecting: false) — jangan mengasumsikan seluruhnya “masuk”. Kedua, callback kita tidak berjalan seketika saat elemen bergerak; ia berjalan pada titik yang ditentukan browser, dan jeda kecil itu tidak pernah terasa bagi pengunjung karena dilindungi margin dan ambang yang kita tetapkan.
Opsi Observer dan Isi Entry #
Dua opsi menentukan kapan callback dianggap perlu dipanggil:
| Opsi | Kegunaan |
|---|---|
root | elemen pembanding; tanpa opsi ini (atau null), pembandingnya viewport |
rootMargin | perluasan area pembanding, ditulis seperti margin CSS, misalnya "200px" atau "0px 0px 100px 0px" |
threshold | ambang perpotongan: 0 (default) berarti sekadar bersentuhan, 0.5 berarti separuh elemen terlihat, 1 berarti utuh |
Setiap entry pada callback membawa laporan lengkap perpotongan. Yang paling sering dipakai: entry.target (elemen yang berubah status), entry.isIntersecting (sedang terlihat atau tidak), entry.intersectionRatio (porsi elemen yang terlihat, dari 0 sampai 1), dan entry.time (waktu relatif terhadap pendaftaran). Dengan threshold: 0.5 dan intersectionRatio, misalnya, animasi “muncul saat terlihat” hanya dijalankan setelah separuh kartu benar-benar masuk layar.
Opsi threshold juga menerima array, bukan satu angka saja: threshold: [0, 0.5, 1] memicu callback setiap kali porsi terlihat melewati 0%, 50%, atau 100%. Dari kombinasi ambang dan intersectionRatio itulah variasi perilaku dibangun — misalnya memutar video hanya bila seluruhnya terlihat, lalu menghentikannya saat porsinya turun di bawah separuh. Sebaliknya, rootMargin bernilai negatif memperkecil area pembanding: "-100px" membuat elemen dianggap “terlihat” hanya setelah menembus 100 pixel ke dalam viewport — pas untuk menghitung tampilan iklan yang sebenarnya dibaca.
Method pengelolaannya lengkap: observe() menambah target, unobserve(elemen) melepas satu target, dan disconnect() menghentikan seluruh pemantauan sekaligus — pas untuk halaman katalog yang diganti kontennya.
Kombinasi data-sumber, callback isIntersecting, dan unobserve di chapter ini adalah pola lazy loading yang dipakai luas; sejak beberapa versi terakhir browser juga menyediakan atribut loading="lazy" pada img untuk kasus sederhana tanpa JavaScript. Untuk kendali penuh — margin khusus, ambang khusus, target selain gambar — IntersectionObserver tetap alatnya.
Observer yang sama melayani tiga pola lain yang sering ditemui. Infinite scroll mengamati satu elemen penanda di dasar daftar: ketika penanda itu masuk viewport, halaman memuat data halaman berikutnya — tanpa tombol “muat lagi”. Animasi masuk (reveal on scroll) mengamati kartu-kartu konten dan menambah class animasi saat isIntersecting bernilai true, dengan unobserve sesudahnya agar animasi cukup sekali. Penghitungan tampilan (misalnya statistik iklan) mengamati unit iklan dengan ambang ketat seperti threshold: 0.5 agar penghitungan hanya berjalan bila unit benar-benar terlihat. Ketiganya memakai object observer yang sama dengan lazy loading di atas; yang berbeda hanya target, opsi, dan aksi di dalam callback.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
lazy-load-gambar.js
Unduh berkas// lazy-load-gambar.js — memuat gambar saat mendekati viewport
// IntersectionObserver hanya ada di browser;
// berkas ini lolos node --check dan dijalankan pada halaman katalog.
const gambarBuku = document.querySelectorAll("img[data-sumber]");
const pengamat = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.sumber;
entry.target.removeAttribute("data-sumber");
pengamat.unobserve(entry.target); // cukup satu kali per gambar
console.log("Dimuat:", entry.target.alt);
}
}
},
{ rootMargin: "200px" } // mulai 200px sebelum gambar benar-benar terlihat
);
for (const img of gambarBuku) {
pengamat.observe(img);
}