WEB-PERFORMANCE BROWSER-INTERNALS UI-UX JAVASCRIPT OPTIMIZATION RENDERING FRONTEND SCHEDULING BATCHING SMOOTH-UI USER-EXPERIENCE WEB-DEVELOPMENT BEST-PRACTICES

Batching dan Scheduling Update di Browser: Kunci UI Super Mulus di Aplikasi Web Modern

⏱️ 6 menit baca
👨‍💻

Batching dan Scheduling Update di Browser: Kunci UI Super Mulus di Aplikasi Web Modern

1. Pendahuluan

Pernahkah Anda merasa frustrasi saat aplikasi web yang Anda bangun terasa lambat, laggy, atau “jank”? Terutama ketika banyak elemen di layar berubah secara bersamaan, atau saat pengguna berinteraksi dengan cepat? Masalah ini seringkali berakar pada cara kita mengelola update Antarmuka Pengguna (UI) dan bagaimana browser memproses perubahan tersebut.

Di balik layar, browser melakukan banyak pekerjaan cerdas untuk membuat aplikasi kita terasa mulus. Salah satu mekanisme penting adalah batching dan scheduling update rendering. Ini adalah proses mengumpulkan berbagai perubahan DOM (Document Object Model) dan CSS, lalu memprosesnya secara efisien dalam satu siklus rendering, daripada memperbarui layar setiap kali ada perubahan kecil.

Memahami bagaimana browser melakukan batching ini, dan kapan kita bisa campur tangan untuk membantu, adalah kunci untuk membangun UI yang super responsif dan bebas jank. Artikel ini akan membawa Anda menyelami mekanisme internal browser dan memberikan strategi praktis untuk mengoptimalkan update UI di aplikasi web modern Anda. Mari kita singkirkan jank dan ciptakan pengalaman pengguna yang mulus! 🎯

2. Memahami Siklus Rendering Browser

Sebelum kita masuk ke batching, mari kita ingat kembali bagaimana browser mengubah kode HTML, CSS, dan JavaScript menjadi piksel yang Anda lihat di layar. Ini adalah siklus yang kompleks, tetapi kita bisa menyederhanakannya menjadi beberapa tahap utama:

  1. Parsing: Browser membaca HTML dan CSS, membuat DOM Tree dan CSSOM Tree.
  2. Style: Menggabungkan DOM dan CSSOM untuk menghitung gaya akhir setiap elemen.
  3. Layout (Reflow): Menghitung posisi dan ukuran setiap elemen di layar. Setiap kali posisi atau ukuran elemen berubah, browser mungkin perlu menghitung ulang layout untuk banyak elemen lain. Ini adalah tahap yang mahal!
  4. Paint: Mengisi piksel untuk setiap elemen (warna, gambar, teks, shadow, dll.).
  5. Composite: Menggambar lapisan-lapisan elemen di layar dalam urutan yang benar, memastikan elemen yang tumpang tindih ditampilkan dengan benar.

⚠️ Penting: Tahap Layout dan Paint adalah yang paling memakan waktu. Setiap kali Anda memodifikasi DOM atau CSS yang memengaruhi layout atau style, browser harus menjalankan ulang sebagian atau seluruh siklus ini. Jika ini terjadi terlalu sering atau tidak efisien, UI Anda akan terlihat patah-patah (jank).

3. Masalah “Layout Thrashing” dan Solusinya

Salah satu penyebab utama jank adalah “Layout Thrashing”. Ini terjadi ketika JavaScript secara bergantian membaca properti layout (seperti offsetWidth, clientHeight, getComputedStyle()) dan kemudian menulis properti layout (seperti element.style.width = '...', element.classList.add('...')).

Contoh buruk Layout Thrashing:

// ❌ Ini menyebabkan layout thrashing!
function updateManyElementsBadly() {
  const elements = document.querySelectorAll('.item');
  for (let i = 0; i < elements.length; i++) {
    // Membaca properti layout, memicu layout
    const width = elements[i].offsetWidth; 
    // Menulis properti layout, memicu layout (lagi!)
    elements[i].style.height = width + 'px'; 
  }
}

Setiap iterasi loop di atas memaksa browser untuk menghitung ulang layout secara sinkron. Bayangkan jika ada ratusan elemen!

Solusi: Pisahkan pembacaan dan penulisan DOM. Lakukan semua pembacaan terlebih dahulu, lalu semua penulisan.

// ✅ Solusi yang lebih baik: Pisahkan baca dan tulis
function updateManyElementsBetter() {
  const elements = document.querySelectorAll('.item');
  const widths = [];

  // Tahap 1: Baca semua properti layout
  for (let i = 0; i < elements.length; i++) {
    widths.push(elements[i].offsetWidth);
  }

  // Tahap 2: Tulis semua properti layout
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.height = widths[i] + 'px';
  }
}

Ini adalah bentuk dasar dari “batching” yang kita lakukan secara manual untuk menghindari thrashing. Browser dapat mengoptimalkan ini karena semua pembacaan dapat dilakukan dalam satu layout pass, dan semua penulisan dalam layout pass berikutnya.

4. Kekuatan Batching Alami Browser dan requestAnimationFrame

Browser secara alami memiliki mekanisme batching yang cerdas. Semua perubahan DOM yang Anda lakukan dalam satu microtask atau macrotask (misalnya, dalam satu eksekusi fungsi JavaScript, atau dalam callback setTimeout atau Promise.then) akan dikumpulkan. Browser kemudian akan menunggu hingga stack eksekusi JavaScript kosong sebelum menjalankan siklus rendering berikutnya.

Ini berarti:

// Browser akan mem-batch kedua perubahan ini
// dan hanya akan me-render sekali setelah fungsi selesai
element.style.width = '100px';
element.style.height = '200px';

💡 Tips: Manfaatkan batching alami ini. Usahakan untuk melakukan semua perubahan DOM yang terkait dalam satu blok kode sinkron.

Namun, bagaimana jika Anda perlu melakukan update UI yang kompleks dan ingin memastikan update tersebut terjadi tepat sebelum frame berikutnya digambar? Di sinilah requestAnimationFrame (rAF) berperan.

requestAnimationFrame memberitahu browser bahwa Anda ingin menjalankan fungsi tertentu tepat sebelum browser akan melakukan siklus rendering berikutnya. Ini adalah cara terbaik untuk:

function animateElement() {
  // Lakukan perubahan DOM atau perhitungan animasi di sini
  element.style.transform = `translateX(${currentPosition}px)`;

  // Minta browser untuk memanggil fungsi ini lagi sebelum frame berikutnya
  requestAnimationFrame(animateElement);
}

// Mulai animasi
requestAnimationFrame(animateElement);

Dengan rAF, browser dapat mem-batch semua update yang diminta dalam callback rAF ke dalam satu siklus rendering yang dioptimalkan, memastikan animasi berjalan pada 60fps (atau sesuai refresh rate monitor).

5. Strategi Batching Manual untuk Interaksi Kompleks

Meskipun browser cerdas, ada kalanya kita perlu secara eksplisit membantu browser untuk mem-batch update, terutama pada interaksi yang sangat kompleks atau saat kita mengelola banyak komponen UI.

📌 Skenario: Anda memiliki daftar item yang banyak, dan setiap item dapat diperbarui secara individual, tetapi Anda ingin semua pembaruan tersebut terjadi dalam satu frame rendering untuk menghindari jank.

Kita bisa menggunakan requestAnimationFrame sebagai “scheduler” manual:

let pendingUpdates = new Map(); // Menggunakan Map untuk menyimpan update yang akan dibatch
let frameRequested = false;

function scheduleUpdate(elementId, newProps) {
  pendingUpdates.set(elementId, { ...pendingUpdates.get(elementId), ...newProps });
  if (!frameRequested) {
    requestAnimationFrame(processPendingUpdates);
    frameRequested = true;
  }
}

function processPendingUpdates() {
  // Lakukan semua update DOM di sini
  pendingUpdates.forEach((props, elementId) => {
    const element = document.getElementById(elementId);
    if (element) {
      // Contoh: Update style
      if (props.color) element.style.color = props.color;
      if (props.text) element.textContent = props.text;
      // ... update properti lainnya
    }
  });

  pendingUpdates.clear();
  frameRequested = false;
}

// ✅ Contoh Penggunaan:
// Panggil scheduleUpdate berkali-kali, tapi rendering hanya akan terjadi sekali per frame
scheduleUpdate('item-1', { color: 'red' });
scheduleUpdate('item-2', { text: 'New Text' });
scheduleUpdate('item-1', { text: 'Updated Item 1' }); // Akan menimpa update sebelumnya untuk item-1

// Semua update di atas akan dibatch dan dieksekusi dalam satu requestAnimationFrame callback.

Dengan pola ini, Anda memastikan bahwa semua perubahan DOM