Kode yang cepat dieksekusi pun bisa terasa lambat bila setiap perubahan DOM memicu kerja ulang tata letak. Browser tidak sekadar mengubah data saat kita menyisipkan elemen atau mengubah gaya — ia harus memastikan apa yang tampil di layar tetap akurat, dan pekerjaan itu berulang setiap kali kita menyentuh dokumen yang sedang tampil.
Chapter ini menata ulang pemahaman kita tentang biaya update DOM. Karena lingkungan pengujian terminal (jsdom) tidak memiliki mesin layout, kita tidak mengukur waktu render dari terminal; sebagai gantinya, kita menghitung jumlah operasi yang menyentuh dokumen — angka yang deterministik dan menjadi patokan biaya yang sah.
Reflow dan Repaint: Pekerjaan di Balik Update #
Setelah DOM berubah, browser menjalankan rangkaian kerja internal: menghitung ulang gaya (style) node yang terkena dampak, menghitung ulang tata letak (layout) untuk menentukan posisi dan ukuran, lalu menggambar ulang (paint) bagian yang berubah sebelum akhirnya disusun ke layar. Dua istilah yang sering dipakai:
- Reflow: penghitungan ulang tata letak. Terjadi bila sesuatu memengaruhi geometri — ukuran, posisi, penambahan atau penghapusan node.
- Repaint: penggambaran ulang tampilan tanpa mengubah tata letak — misalnya mengganti warna latar.
Reflow lebih mahal dan bisa berantai: menambah satu <li> ke daftar memengaruhi posisi daftar itu dan seluruh elemen di bawahnya. Karena itu jumlah gelombang reflow lebih menentukan daripada jumlah perubahan kecilnya — seratus perubahan yang dipadatkan menjadi satu gelombang lebih hemat daripada seratus gelombang kecil.
Polanya sudah kita kenal dari chapter DOM Fragments: menyusun elemen di DocumentFragment di luar dokumen, lalu menempelkannya sekali. Kali ini kita membuktikan klaim biayanya dengan penghitung.
Menghitung Penempelan: Loop Langsung vs Fragment #
Pertanyaannya: berapa kali menyentuh dokumen yang sedang tampil? Untuk menghitungnya, kita mengganti append milik elemen daftar dengan pembungkus penghitung — setiap penempelan ke daftar itu dicatat, sisanya diteruskan ke append asli:
// susun-daftar.js — menghitung penempelan ke dokumen, bukan mengukur waktu
const data = [];
for (let i = 1; i <= 200; i++) {
data.push("Item " + i);
}
function buatItem(teks) {
const li = document.createElement("li");
li.textContent = teks;
return li;
}
// --- versi 1: append langsung ke dokumen ---
const daftarA = document.querySelector("#daftar-a");
let penempelanA = 0;
const appendA = daftarA.append.bind(daftarA);
daftarA.append = (...anak) => {
penempelanA++;
appendA(...anak);
};
for (const teks of data) {
daftarA.append(buatItem(teks));
}
console.log("versi langsung : penempelan", penempelanA, "— isi", daftarA.children.length);
// --- versi 2: susun di fragment, tempel sekali ---
const daftarB = document.querySelector("#daftar-b");
let penempelanB = 0;
const appendB = daftarB.append.bind(daftarB);
daftarB.append = (...anak) => {
penempelanB++;
appendB(...anak);
};
const fragmen = document.createDocumentFragment();
for (const teks of data) {
fragmen.append(buatItem(teks));
}
console.log("sebelum ditempel: isi dokumen", daftarB.children.length);
daftarB.append(fragmen);
console.log("versi fragment : penempelan", penempelanB, "— isi", daftarB.children.length);Output yang Dihasilkan (pada tab Console browser):
versi langsung : penempelan 200 — isi 200
sebelum ditempel: isi dokumen 0
versi fragment : penempelan 1 — isi 200Mari kita telaah baris-baris pentingnya:
- Baris 16–20 (pembungkus penghitung):
bindmenyimpanappendasli milikdaftarAke variabelappendA, lalu propertiappendpada elemen itu diganti function baru yang mencatat penghitung sebelum meneruskan panggilan. Trik ini membuat setiap penempelan ke daftar tercatat tanpa mengubah cara pemanggilannya. - Baris 22–25 (loop langsung): 200 kali
daftarA.append(...)— dan keluarannya mencatatpenempelan 200. Di browser sungguhan, itu berarti hingga 200 gelombang kerja layout untuk dokumen yang sedang tampil. - Baris 36–39 (menyusun di fragment): 200 kali
appendjuga terjadi — tetapi tujuannya fragment, wadah di luar dokumen. Baris keluaran tengah membuktikannya:isi dokumen 0, tidak ada satu pun node yang menyentuh halaman selama loop. - Baris 40–41 (satu penempelan):
daftarB.append(fragmen)tercatat sebagai penempelan tunggal, dan isi daftar langsung lengkap 200 item. Seratus atau seribu item pun angkanya tetap1.
Angka 200 melawan 1 adalah bukti terukur untuk klaim di chapter DOM Fragments: fragment memadatkan banyak penyisipan menjadi satu momen yang menyentuh dokumen. Di browser, satu momen itu berarti satu kesempatan menghitung ulang layout — bukan seratus.
Layout Thrashing: Baca-Tulis Bergantian #
Bentuk paling menyita waktu di lapisan render terjadi bukan karena banyak menulis, melainkan karena baca dan tulis bergantian. Bacaan geometri seperti offsetHeight atau getBoundingClientRect() butuh nilai tata letak terkini; bila dibaca setelah ada tulisan yang belum dihitung, browser terpaksa menjalankan reflow saat itu juga agar angkanya benar. Loop yang menulis lalu membaca setiap iterasi memaksa reflow sinkron sebanyak iterasinya:
// Pola merugikan: tiap iterasi memaksa reflow sinkron
for (const item of daftarItem) {
item.style.width = "50%"; // tulisan: angka layout basi
tinggiTotal += item.offsetHeight; // bacaan: reflow dipaksa sekarang
}
// Pola sehat: semua bacaan dulu, lalu semua tulisan
const tinggiSemua = daftarItem.map((item) => item.offsetHeight);
for (const item of daftarItem) {
item.style.width = "50%";
}Versi pertama menulis gaya lalu membaca tinggi di iterasi yang sama; versi kedua mengumpulkan seluruh bacaan lebih dulu, sehingga reflow hanya dipicu satu kali setelah semua tulisan selesai. Pola perbaikannya bisa diingat satu frasa: batch bacaan, batch tulisan, jangan selang-seling.
Perlu dinyatakan jujur: contoh di atas adalah perilaku browser, dan tidak bisa diuji dari terminal — di jsdom, offsetHeight selalu 0 karena tidak ada mesin layout. Untuk mengamatinya sungguhan, jalankan potongan serupa di tab Performance DevTools dan cari tugas-tugas layout yang berulang setiap iterasi. Yang tetap bisa kita uji dari terminal adalah jumlah penempelan ke dokumen — dan di situlah fragment terbukti unggul sebagai angka.
Menyiram Tulisan Sekali per Frame #
Bacaan geometri bukan satu-satunya yang perlu diatur; penjadwalan tulisan pun punya alat bawaan browser: requestAnimationFrame. Callback yang diserahkan ke sana dijalankan browser tepat sebelum frame berikutnya digambar — bukan seketika saat dipanggil. Sifat ini membuatnya cocok sebagai pintu penyiram: tulisan yang datang bertubi-tubi ditumpuk dulu, lalu semua dieksekusi dalam satu gelombang tepat sebelum gambar ulang.
Berkas berikut menguji pola itu. Tiga tulisan dijadwalkan berturut-turut, tetapi hanya callback rAF pertama yang benar-benar terdaftar — sisanya menunggu di antrian sampai gelombang itu menyala:
// tulisan-terjadwal.js — menumpuk tulisan, menyiram sekali per frame
const kotak = document.querySelector("#kotak");
let antrian = [];
let terjadwal = false;
function jadwalkan(kerja) {
antrian.push(kerja);
if (!terjadwal) {
terjadwal = true;
window.requestAnimationFrame(() => {
const tugas = antrian;
antrian = [];
terjadwal = false;
for (const t of tugas) {
t();
}
console.log("satu gelombang menyiram:", tugas.length, "tulisan");
});
}
}
jadwalkan(() => { kotak.textContent = "B"; });
jadwalkan(() => { kotak.style.color = "blue"; });
jadwalkan(() => { kotak.textContent = "C"; });
console.log("sebelum frame : isi", kotak.textContent);Output yang Dihasilkan (pada tab Console browser):
sebelum frame : isi A
satu gelombang menyiram: 3 tulisanMari kita telaah baris-baris pentingnya:
- Baris 6–20 (
jadwalkan): setiap tugas tulisan ditumpuk keantrian(baris 7). Baris 8 (if (!terjadwal)) menjamin hanya callback rAF pertama yang terdaftar — tugas-tugas berikutnya menumpuk tanpa menambah penjadwalan. - Baris 10–18 (callback rAF): saat browser siap menggambar frame berikutnya, callback ini berjalan: antrian disalin ke
tugas(baris 11), antrian dan penanda direset (baris 12–13), lalu seluruh tugas dieksekusi berurutan (baris 14–16) dan jumlahnya dicetak. - Baris 22–25 (tiga tugas dan pencetakan pertama): ketiga tulisan dijadwalkan tanpa jeda — semuanya masuk antrian sebelum callback rAF sempat berjalan. Baris 25 berjalan sinkron tepat setelahnya, sehingga tercetak lebih dulu.
- Keluaran dua barisnya: baris pertama membuktikan tulisan belum menyentuh dokumen (
isi A, masih nilai awal) meski ketiga tugas sudah dijadwalkan; baris kedua membuktikan ketiganya disiram dalam satu gelombang. Tiga tulisan, satu momen eksekusi.
Pola ini adalah pasangan dari fragment: fragment memadatkan penyisipan node, requestAnimationFrame memadatkan perubahan pada node yang sudah ada. Keduanya mengejar tujuan yang sama — memastikan pekerjaan render terjadi dalam gelombang yang sedikit dan terencana, bukan berserakan. Pada interaksi berfrekuensi tinggi seperti menggambar mengikuti gerakan pointer, pola inilah yang menyelamatkan kenyamanan.
Kini seluruh lapisan biaya sudah terpetakan: pemuatan, memori, dan rendering. Satu langkah terakhir menutup section ini — menyusun semuanya menjadi protokol pengukuran sebelum–sesudah yang disiplin.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
susun-daftar.js
Unduh berkas// susun-daftar.js — menghitung penempelan ke dokumen, bukan mengukur waktu
const data = [];
for (let i = 1; i <= 200; i++) {
data.push("Item " + i);
}
function buatItem(teks) {
const li = document.createElement("li");
li.textContent = teks;
return li;
}
// --- versi 1: append langsung ke dokumen ---
const daftarA = document.querySelector("#daftar-a");
let penempelanA = 0;
const appendA = daftarA.append.bind(daftarA);
daftarA.append = (...anak) => {
penempelanA++;
appendA(...anak);
};
for (const teks of data) {
daftarA.append(buatItem(teks));
}
console.log("versi langsung : penempelan", penempelanA, "— isi", daftarA.children.length);
// --- versi 2: susun di fragment, tempel sekali ---
const daftarB = document.querySelector("#daftar-b");
let penempelanB = 0;
const appendB = daftarB.append.bind(daftarB);
daftarB.append = (...anak) => {
penempelanB++;
appendB(...anak);
};
const fragmen = document.createDocumentFragment();
for (const teks of data) {
fragmen.append(buatItem(teks));
}
console.log("sebelum ditempel: isi dokumen", daftarB.children.length);
daftarB.append(fragmen);
console.log("versi fragment : penempelan", penempelanB, "— isi", daftarB.children.length);
tulisan-terjadwal.js
Unduh berkas// tulisan-terjadwal.js — menumpuk tulisan, menyiram sekali per frame
const kotak = document.querySelector("#kotak");
let antrian = [];
let terjadwal = false;
function jadwalkan(kerja) {
antrian.push(kerja);
if (!terjadwal) {
terjadwal = true;
window.requestAnimationFrame(() => {
const tugas = antrian;
antrian = [];
terjadwal = false;
for (const t of tugas) {
t();
}
console.log("satu gelombang menyiram:", tugas.length, "tulisan");
});
}
}
jadwalkan(() => { kotak.textContent = "B"; });
jadwalkan(() => { kotak.style.color = "blue"; });
jadwalkan(() => { kotak.textContent = "C"; });
console.log("sebelum frame : isi", kotak.textContent);