Mengapa window.addEventListener("scroll", ...) dan getBoundingClientRect() Buruk bagi Performa?
Di masa lalu, untuk mengetahui apakah pengguna sudah menggulir sampai ke bagian bawah daftar (infinite scroll) atau untuk menandai bab mana yang sedang aktif di daftar isi samping (scrollspy), pengembang memasang window.addEventListener("scroll", ...), lalu di dalamnya memanggil elemen.getBoundingClientRect() puluhan kali per detik.
Masalahnya:
- Event
scrollditembakkan secara bertubi-tubi pada utas utama (main thread). - Memanggil
getBoundingClientRect()memaksa peramban menghitung ulang tata letak seluruh halaman secara sinkron (Forced Synchronous Layout), sehingga guliran layar terasa patah-patah!
Peramban modern menyediakan tiga Observer API asinkron yang sangat hemat baterai dan tidak pernah memblokir guliran layar!
1. IntersectionObserver: Mendeteksi Kapan Elemen Masuk atau Keluar Layar
IntersectionObserver (status Baseline Widely Available) memungkinkan Anda mendaftarkan fungsi callback yang hanya dipanggil ketika sebuah elemen target mulai masuk atau keluar dari area pandang layar (atau wadah gulir tertentu):
// Contoh Nyata: Menandai bab yang sedang dibaca pada Daftar Isi samping (Scrollspy)
// dan memuat data tambahan saat elemen pemicu di bagian bawah terlihat!
const daftarBab = document.querySelectorAll("main section[id]");
const pengamatVisibilitas = new IntersectionObserver(
(daftarEntri) => {
for (const entri of daftarEntri) {
// entri.isIntersecting bernilai true jika elemen masuk ke zona pengamatan
if (entri.isIntersecting) {
const idAktif = entri.target.id;
console.log(`Pengguna sedang membaca bagian: #${idAktif}`);
}
}
},
{
root: null, // null berarti mengamati terhadap viewport layar utama
rootMargin: "-10% 0px -70% 0px", // Memfokuskan zona deteksi di sepertiga atas layar
threshold: 0 // Panggil segera begitu 1 piksel pertama menyentuh zona
}
);
// Daftarkan seluruh elemen <section> untuk diamati oleh SATU instance observer!
for (const bab of daftarBab) {
pengamatVisibilitas.observe(bab);
}
//Catatan: Jika komponen dihapus, hentikan pengamatan dengan pengamatVisibilitas.disconnect()
Kegunaan Utama IntersectionObserver:
- Infinite Scroll: Letakkan satu elemen kosong
<div id="pemicu-muat-lagi"></div>di bawah daftar kartu; ketikaentri.isIntersectingbernilaitrue, panggilfetch()untuk memuat 10 kartu berikutnya! - Menjeda Video/Animasi Otomatis: Ketika pemutar video atau kanvas simulasi keluar dari layar (
!entri.isIntersecting), panggilvideo.pause()untuk menghemat CPU pengguna.
2. ResizeObserver: Merespons Perubahan Ukuran Elemen Individual
Berbeda dengan window.addEventListener("resize", ...) yang hanya memberi tahu jika jendela peramban secara keseluruhan diubah ukurannya, ResizeObserver (status Baseline Widely Available) memberi tahu Anda setiap kali kotak elemen tertentu berubah ukurannya (misalnya karena bilah samping ditutup, teks baru dimasukkan, atau tata letak Grid bergeser).
Ini sangat penting ketika Anda menggambar grafik di dalam <canvas> yang harus menyesuaikan jumlah pikselnya dengan ukuran wadah pembungkusnya:
const wadahGrafik = document.querySelector("#wadah-kanvas");
const kanvas = wadahGrafik.querySelector("canvas");
const pengamatUkuran = new ResizeObserver((daftarEntri) => {
for (const entri of daftarEntri) {
// Membaca lebar dan tinggi border-box elemen yang sudah dihitung peramban
// (Tanpa memicu Forced Synchronous Layout!)
const { inlineSize: lebar, blockSize: tinggi } = entri.borderBoxSize[0];
kanvas.width = Math.round(lebar * window.devicePixelRatio);
kanvas.height = Math.round(tinggi * window.devicePixelRatio);
gambarUlangGrafik(kanvas);
}
});
pengamatUkuran.observe(wadahGrafik);
3. MutationObserver: Mengamati Perubahan Struktur atau Atribut Pohon DOM
Bagaimana jika Anda perlu menjalankan logika tertentu ketika kode lain menambahkan elemen baru ke dalam sebuah wadah atau ketika atribut data-tema pada elemen <html> berubah? Gunakan MutationObserver:
const pengamatDom = new MutationObserver((daftarMutasi) => {
for (const mutasi of daftarMutasi) {
if (mutasi.type === "attributes" && mutasi.attributeName === "data-tema") {
const temaBaru = document.documentElement.getAttribute("data-tema");
console.log(`Tema dokumen berubah menjadi: ${temaBaru}`);
}
}
});
pengamatDom.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-tema"] // Hanya amati atribut yang relevan agar tetap ringan!
});
Tabel Pemilihan Tiga Observer API
| Kebutuhan Anda | Gunakan Apa? |
|---|---|
| Mengubah tata letak CSS komponen berdasarkan lebar wadahnya | CSS @container Queries (Utamakan CSS sebelum JS!). |
Menyesuaikan ukuran <canvas> atau logika JS saat ukuran sebuah elemen berubah | ResizeObserver |
| Mendeteksi apakah sebuah elemen sedang terlihat di layar (infinite scroll, scrollspy, analitik impresi) | IntersectionObserver |
| Mendeteksi perubahan atribut atau penambahan/penghapusan simpul anak di pohon DOM | MutationObserver |
Latihan Praktik: Daftar Isi Scrollspy dan Pemuat Kartu Otomatis dengan IntersectionObserver
Buatlah berkas observer-latihan.html dan observer.js:
- Buat 4
<section id="bab-1">s.d.bab-4yang masing-masing setinggi minimal70vh, serta navigasi daftar isi di sampingnya. - Gunakan satu
IntersectionObserveruntuk memindahkan atributaria-current="location"secara otomatis ke tautan daftar isi yang sesuai dengan bab yang sedang terlihat di layar.