DOM adalah pohon elemen halaman yang dipegang browser, yang bisa dibaca dan diubah JavaScript. Tiap kali ukuran atau isi elemen berubah, browser harus menghitung ulang layout, yaitu posisi dan ukuran semua kotak di halaman, sebelum menggambarnya. Manipulasi DOM yang efisien artinya bikin hitungan ulang itu sesedikit mungkin. Bayangin kamu menata ulang perabot kamar sambil ada yang terus nanya "lebar lorongnya sekarang berapa?". Kalau tiap habis geser satu lemari kamu harus mengukur ulang seluruh kamar, capek. Lebih hemat: ukur semuanya sekali, catat, baru geser semua perabot.
🧪 Tebakan cepat: ada 100 kotak. Untuk tiap kotak, loop-nya membaca
offsetHeightlalu langsung menulisstyle.height. Berapa kali browser menghitung layout: 1, 2, atau 100? Jawabannya ada di bagian "Baca dulu semua, baru tulis semua".
Baca dan tulis di DOM
Kode yang menyentuh DOM melakukan dua hal yang beda:
- Tulis: mengubah sesuatu yang memengaruhi tampilan, misalnya
el.style.height = "20px",el.classList.add("besar"), atau menambah elemen. Browser gak langsung menghitung layout. Dia cuma menandai "layout-nya sudah basi", lalu menunggu sampai waktunya menggambar frame berikutnya. - Baca ukuran atau posisi:
offsetHeight,offsetWidth,offsetTop,clientWidth,scrollTop,getBoundingClientRect(), ataugetComputedStyle(). Supaya jawabannya benar, browser butuh layout yang segar.
Kalau kamu membaca waktu layout-nya basi, browser terpaksa menghitung layout saat itu juga, di tengah kode JavaScript-mu. Ini namanya forced synchronous layout (layout yang dipaksa). Baca dan tulis yang selang-seling berkali-kali, sampai layout dipaksa terus, disebut layout thrashing.
Baca dulu semua, baru tulis semua
Contohnya tiga batang dengan tinggi 5, 10, dan 3 px. Tiap tinggi mau diubah jadi tinggi * 2 + 10, jadi hasilnya 20, 30, dan 16 px. Halamannya lagi tenang, jadi layout awalnya segar.
| Langkah | Selang-seling | Batch |
|---|---|---|
| 1 | baca batang 1 (layout segar) | baca batang 1 |
| 2 | tulis batang 1, layout basi | baca batang 2 |
| 3 | baca batang 2: layout dipaksa | baca batang 3 |
| 4 | tulis batang 2, layout basi | tulis batang 1 |
| 5 | baca batang 3: layout dipaksa | tulis batang 2 |
| 6 | tulis batang 3 | tulis batang 3 |
| Layout dipaksa | 2 kali | 0 kali |
Sesudah loop, dua-duanya butuh satu layout lagi waktu frame berikutnya digambar. Dengan 100 kotak, versi selang-seling memaksa layout 99 kali, ditambah 1 kali di frame berikutnya: total 100. Versi batch cuma 1. Itu jawaban tebakannya: 100 lawan 1.
// Selang-seling: tiap baca setelah tulis memaksa layout
const bars = [...document.querySelectorAll(".bar")]; // tinggi 5, 10, 3
bars.forEach((bar) => {
const h = bar.offsetHeight; // baca
bar.style.height = h * 2 + 10 + "px"; // tulis
});
// Batch: baca semua dulu, baru tulis semua
const bars = [...document.querySelectorAll(".bar")];
const heights = bars.map((bar) => bar.offsetHeight); // [5, 10, 3]
bars.forEach((bar, i) => {
bar.style.height = heights[i] * 2 + 10 + "px"; // 20, 30, 16
});
[...document.querySelectorAll(".bar")]mengubah NodeList jadi array biasa, supaya bisa pakaimap.bars.map((bar) => bar.offsetHeight)membaca semua tinggi selagi layout masih segar. Gak ada tulisan di antaranya, jadi gak ada layout yang dipaksa.- Loop kedua cuma menulis. Browser menandai layout basi, lalu menghitungnya sekali waktu menggambar.
Latihan memodelkan pola ini tanpa DOM: tiap elemen jadi object { value }.
function batchUpdate(elements) {
const values = elements.map((element) => element.value); // 1. baca semua
return values.map((value) => ({ value: value * 2 + 10 })); // 2. tulis semua
}
console.log(batchUpdate([{ value: 5 }, { value: 10 }, { value: 3 }]));
// [{ value: 20 }, { value: 30 }, { value: 16 }]
mapyang pertama mengumpulkan angkanya saja:[5, 10, 3].mapyang kedua bikin array baru berisi object baru.({ value: ... })dibungkus kurung supaya kurung kurawalnya dibaca sebagai object, bukan badan function.batchUpdate([{ value: 0 }])hasilnya[{ value: 10 }], karena 0 kali 2 tambah 10 sama dengan 10.
Menambah banyak elemen sekaligus
Menambah 1.000 <li> satu per satu ke list yang tampil gak bikin 1.000 kali layout, selama di dalam loop gak ada yang membaca ukuran. Layout tetap ditunda sampai frame berikutnya. Tapi tiap elemen yang masuk ke halaman tetap menambah kerja kecil: browser mencatat perubahannya dan menandai style yang perlu dihitung ulang. DocumentFragment adalah wadah ringan yang hidup di memori, bukan di halaman. Kumpulkan semua elemen di situ, lalu masukkan ke halaman sekali.
const list = document.querySelector("#list");
const fragment = document.createDocumentFragment(); // wadah di memori
for (let i = 1; i <= 1000; i++) {
const li = document.createElement("li");
li.textContent = "Item " + i;
fragment.appendChild(li); // halaman belum berubah
}
list.appendChild(fragment); // 1.000 <li> masuk sekali jalan
- Selama loop, halaman belum tersentuh sama sekali.
list.appendChild(fragment)memindahkan semua anak fragment ke list. Fragment-nya sendiri gak ikut masuk, dan sesudahnya kosong.- Cara lain yang setara: kumpulkan elemennya di array, lalu
list.append(...items).
textContent atau innerHTML
el.textContent = teksmemasang teks apa adanya. Gak ada HTML yang diproses, jadi cepat, dan<b>tampil sebagai tulisan<b>.el.innerHTML = teksmemproses string itu sebagai HTML. Praktis untuk memasang banyak markup sekaligus, tapi berbahaya kalau isinya dari user: nama seperti<img src=x onerror="...">akan menjalankan script-nya. Celah ini disebut XSS.
Aturannya: data dari user selalu lewat textContent.
⚠️ Jebakan yang sering ketemu
- Membaca ukuran di dalam loop yang menulis. Sering tersembunyi di function bantu yang memanggil
getBoundingClientRect()ataugetComputedStyle(). Kumpulkan semua bacaan sebelum loop yang menulis. - Mengira tiap
appendChildlangsung bikin layout. Yang mahal adalah membaca ukuran di antara penambahan. DocumentFragment tetap berguna karena halaman cuma berubah sekali. innerHTMLdengan data dari user. PakaitextContent.- Di latihan: lupa
returnatau pakaiforEach.forEachgak mengembalikan apa-apa.batchUpdateharus me-return array baru[{ value: 20 }, { value: 30 }, { value: 16 }], bukan cuma mencetaknya.
TL;DR: Menulis ke DOM cuma menandai layout basi, dan browser menghitungnya sekali waktu menggambar. Membaca ukuran (offsetHeight, getBoundingClientRect()) setelah menulis memaksa layout saat itu juga, dan baca tulis yang selang-seling di loop disebut layout thrashing: 100 kotak jadi 100 kali layout. Baca semua dulu, baru tulis semua, dan layout-nya cuma sekali. Untuk banyak elemen baru, kumpulkan di DocumentFragment lalu masukkan sekali. Teks dari user pakai textContent, bukan innerHTML. Di latihan, batchUpdate membaca semua value, lalu me-return array baru berisi { value: value * 2 + 10 }.