Daftar menu harian toko Budi dirender dari data: satu array berisi nama dan harga, dan setiap elemen array menjadi satu <li>. Dengan createElement dan append dari chapter Creating Elements, pekerjaan itu selesai — tetapi ada satu detail yang baru terasa saat daftarnya panjang: setiap append ke dokumen adalah satu perubahan pada halaman yang sedang tampil, dan ratusan perubahan kecil itu lebih mahal daripada satu perubahan besar.

DocumentFragment adalah jawaban standar untuk masalah ini: wadah sementara di luar pohon dokumen tempat kita menyusun seluruh struktur, yang kemudian ditempelkan ke halaman dalam satu langkah. Chapter ini membedah cara kerjanya, menggunakannya untuk merender list dari data, dan menjelaskan mengapa pola ini menghemat kerja browser.

DocumentFragment: Kontainer di Luar Pohon #

document.createDocumentFragment() membuat object yang terlihat seperti elemen — ia bisa menampung anak dengan append, punya children, bisa dijajal — tetapi dengan satu perbedaan mendasar: fragment bukan bagian dari dokumen. Ia tidak dirender, tidak punya tag, tidak bisa diberi class, dan tidak ditemukan oleh querySelector.

Perilaku saat ditempel juga khas. Bila fragment berisi lima anak dan kita panggil daftar.append(fragmen), yang dipindahkan adalah kelima anaknya — bukan fragment itu sendiri. Setelah penempelan, fragment menjadi wadah kosong yang bisa dipakai ulang untuk batch berikutnya.

html Dua kontainer kosong yang akan diisi dari data
<ul id="menu-siang"></ul>
<ul id="menu-malam"></ul>

Render List dari Data #

Berkas berikut merender array menu ke dua daftar dengan dua cara berbeda: langsung ke dokumen, dan lewat fragment. Hasil akhirnya identik; cara kerja di baliknya yang berbeda.

// render-daftar.js — merender list dari data lewat DocumentFragment

const menu = [
  { nama: "Kopi Gayo", harga: 18000 },
  { nama: "Teh Melati", harga: 12000 },
  { nama: "Gula Aren", harga: 9000 },
  { nama: "Roti Bakar", harga: 15000 },
];

function buatItem(nama, harga) {
  const li = document.createElement("li");
  li.textContent = `${nama} — Rp${harga.toLocaleString("id-ID")}`;
  return li;
}

// 1. langsung ke dokumen: tiap append menyentuh halaman
const daftarSiang = document.querySelector("#menu-siang");
for (const item of menu) {
  daftarSiang.append(buatItem(item.nama, item.harga));
}
console.log(daftarSiang.children.length); // 4

// 2. lewat fragment: disusun di luar dokumen, ditempel sekali
const daftarMalam = document.querySelector("#menu-malam");
const fragmen = document.createDocumentFragment();
for (const item of menu) {
  fragmen.append(buatItem(item.nama, item.harga));
}
console.log(fragmen.children.length);     // 4 — terisi, tapi belum di halaman
daftarMalam.append(fragmen);
console.log(daftarMalam.children.length); // 4 — semua masuk sekaligus
console.log(fragmen.children.length);     // 0 — isi berpindah
console.log(daftarMalam.firstElementChild.textContent);

Output yang Dihasilkan (console browser):

text
4
4
4
0
Kopi Gayo — Rp18.000

Keluaran dicatat dari eksekusi nyata di lingkungan jsdom; format angka Rp18.000 berasal dari toLocaleString("id-ID") yang sudah kita pakai di section Modules. Mari kita bedah:

  1. Baris 12–16 (buatItem): satu function kecil bertugas membuat satu <li> lengkap dengan teksnya, lalu mengembalikannya. Memisahkan pembuatan node ke function sendiri membuat kedua strategi di bawahnya memakai kode yang persis sama.
  2. Baris 19–23 (cara langsung): setiap iterasi memanggil daftarSiang.append(...) — empat kali penyisipan langsung ke dokumen yang sedang tampil. Daftar pun benar, dengan empat perubahan terpisah pada halaman.
  3. Baris 26–31 (menyusun di fragment): struktur yang sama disusun, tetapi tujuannya fragmen. Baris 31 membuktikan fragment memang terisi — children.length bernilai 4 — meski tidak satu pun item itu tampil di halaman.
  4. Baris 32–33 (satu penempelan): daftarMalam.append(fragmen) memindahkan keempat anak sekaligus. Bagi dokumen, ini satu operasi penyisipan dengan empat hasil.
  5. Baris 34–35 (fragment kosong dan hasil akhir): setelah penempelan, fragmen.children.length menjadi 0 — anak-anaknya berpindah, bukan disalin. Pembacaan firstElementChild mengonfirmasi isi daftar malam lengkap dengan format rupiahnya.

Mengapa Fragment Menghemat Kerja Browser #

Untuk memahami manfaatnya, kita perlu melihat sisi yang tidak terlihat: setelah DOM berubah, browser harus memastikan tampilan di layar tetap akurat. Rangkaian kerja internalnya kira-kira: hitung ulang gaya yang terkena dampak, lalu hitung ulang tata letak (layout), lalu gambar ulang (paint) bagian yang berubah. Proses penghitungan ulang tata letak inilah yang disebut reflow.

Titik pentingnya: semua perhitungan itu hanya berlaku untuk node yang berada di dalam dokumen. Node yang masih menumpuk di fragment atau di variabel bebas tidak ikut dihitung, karena tidak ada yang merender mereka. Dengan menyusun dulu di fragment, kita menunda seluruh pekerjaan browser sampai satu momen penempelan — satu gelombang reflow untuk seluruh daftar, bukan satu gelombang per item.

Browser modern menambahkan sejumlah optimasi sendiri untuk pola-pola tertentu, sehingga selisihnya tidak selalu besar pada daftar pendek. Meski begitu, pola fragment tetap layak dijadikan kebiasaan: biayanya hanya satu baris ekstra, dan manfaatnya tumbuh seiring panjang daftar serta kompleksitas elemennya.

Fragment dari Dua Sisi #

Satu nuanse menarik layak dibuktikan: fragment bisa memeriksa isinya sendiri, tetapi dokumen tidak melihat apa pun di dalamnya.

js Fragment terlihat oleh dirinya sendiri, tidak oleh dokumen
const fragmen = document.createDocumentFragment();
const draf = document.createElement("li");
draf.textContent = "Draft menu";
fragmen.append(draf);
console.log(fragmen.querySelectorAll("li").length);  // 1 — dari sisi fragment
console.log(document.querySelectorAll("li").length); // 0 — dokumen tidak melihat

Pencarian dari fragment berjalan normal karena fragment mengimplementasikan method pencarian untuk subtree-nya sendiri — berguna saat kita menyusun struktur besar dan perlu memeriksa keberadaan node di dalamnya sebelum menempel. Sementara document.querySelectorAll tetap menghitung 0 karena tidak ada satu pun node itu yang menempel ke pohon.

Fragment bukan satu-satunya cara menyusun di luar dokumen — alternatifnya adalah elemen sungguhan yang tidak ditempelkan, misalnya <ul> baru dari createElement yang diisi dulu sebelum dipasang. Perbedaan praktisnya: elemen penampung itu akan ikut masuk ke DOM sebagai satu node tambahan, sedangkan fragment menguap setelah penempelan dan menyisakan persis isi yang disusun. Untuk merender daftar ke kontainer yang sudah ada, fragment adalah bentuk yang paling bersih.

Mengukur Manfaatnya di Browser #

Lingkungan pengujian terminal (jsdom) tidak memiliki mesin layout, sehingga reflow tidak bisa diukur dari sana — pengukuran hanya bermakna di browser sungguhan. Pola ukurnya sederhana dengan performance.now():

js Pola pengukuran di browser (angka bervariasi antar mesin)
let awal = performance.now();
for (let i = 0; i < 2000; i++) {
  daftarSiang.append(buatItem("Item Uji", 1000));
}
console.log(`langsung : ${(performance.now() - awal).toFixed(1)} ms`);

awal = performance.now();
const fragmen = document.createDocumentFragment();
for (let i = 0; i < 2000; i++) {
  fragmen.append(buatItem("Item Uji", 1000));
}
daftarMalam.append(fragmen);
console.log(`fragment : ${(performance.now() - awal).toFixed(1)} ms`);

Jalankan potongan ini di console browser pada halaman berisi dua ul kosong, lalu bandingkan dua angkanya. Baca hasilnya dengan dua catatan agar tidak menarik kesimpulan keliru:

  • Angkanya berbeda antar mesin, antar browser, dan antar kali jalankan. Yang menentukan arah kesimpulan adalah selisih yang konsisten pada daftar besar, bukan satu-dua milidetik pada daftar kecil.
  • Pengukuran termasuk pekerjaan pembuatan 2000 node di kedua versi, sehingga yang dibandingkan adalah biaya penempelan ke dokumen — bagian yang membedakan kedua strategi.

Pola fragment menuntaskan sisi efisiensi manipulasi DOM. Satu babak terakhir tersisa di section ini, dan babak itu soal keamanan: apa yang boleh dan tidak boleh disisipkan ke DOM ketika sumber datanya adalah manusia yang tidak kita kenal.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

render-daftar.js

Unduh berkas
javascript
// render-daftar.js — merender list dari data lewat DocumentFragment

const menu = [
  { nama: "Kopi Gayo", harga: 18000 },
  { nama: "Teh Melati", harga: 12000 },
  { nama: "Gula Aren", harga: 9000 },
  { nama: "Roti Bakar", harga: 15000 },
];

function buatItem(nama, harga) {
  const li = document.createElement("li");
  li.textContent = `${nama} — Rp${harga.toLocaleString("id-ID")}`;
  return li;
}

// 1. langsung ke dokumen: tiap append menyentuh halaman
const daftarSiang = document.querySelector("#menu-siang");
for (const item of menu) {
  daftarSiang.append(buatItem(item.nama, item.harga));
}
console.log(daftarSiang.children.length); // 4

// 2. lewat fragment: disusun di luar dokumen, ditempel sekali
const daftarMalam = document.querySelector("#menu-malam");
const fragmen = document.createDocumentFragment();
for (const item of menu) {
  fragmen.append(buatItem(item.nama, item.harga));
}
console.log(fragmen.children.length);     // 4 — terisi, tapi belum di halaman
daftarMalam.append(fragmen);
console.log(daftarMalam.children.length); // 4 — semua masuk sekaligus
console.log(fragmen.children.length);     // 0 — isi berpindah
console.log(daftarMalam.firstElementChild.textContent);