← Kembali

JavaScript Menengah Utility

Debounce function dengan cancel & immediate option

Implementasi debounce vanilla JS dengan opsi cancel pending call dan trigger immediate sebelum delay habis. Tanpa lodash.

Dipublikasikan 20 Mei 2026

Search box, autocomplete, save-as-you-type — semuanya butuh debounce supaya tidak fire request setiap keystroke. Lodash _.debounce bagus, tapi 70 KB minified hanya untuk debounce itu berat. Snippet ini 25 baris, dependency-free, dan ada cancel() method untuk batalkan pending call.

Kode

/**
 * Debounce fungsi — execute setelah `wait` ms tanpa call baru.
 * @template {(...args: any[]) => any} F
 * @param {F} fn - Fungsi yang akan di-debounce.
 * @param {number} wait - Delay dalam ms.
 * @param {object} [options]
 * @param {boolean} [options.leading=false] - Fire sekali di awal sebelum debounce mulai.
 * @returns {F & { cancel: () => void; flush: () => void }}
 */
function debounce(fn, wait, options = {}) {
  let timer = null;
  let lastArgs = null;
  let lastThis = null;
  const { leading = false } = options;

  const debounced = function (...args) {
    lastArgs = args;
    lastThis = this;

    const shouldCallNow = leading && timer === null;

    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!leading && lastArgs) {
        fn.apply(lastThis, lastArgs);
      }
      lastArgs = null;
      lastThis = null;
    }, wait);

    if (shouldCallNow) {
      fn.apply(this, args);
    }
  };

  debounced.cancel = () => {
    clearTimeout(timer);
    timer = null;
    lastArgs = null;
    lastThis = null;
  };

  debounced.flush = () => {
    if (timer !== null && lastArgs) {
      clearTimeout(timer);
      fn.apply(lastThis, lastArgs);
      timer = null;
      lastArgs = null;
      lastThis = null;
    }
  };

  return debounced;
}

Pemakaian

// Search dengan debounce
const search = debounce((query) => {
  fetch(`/api/search?q=${encodeURIComponent(query)}`)
    .then((r) => r.json())
    .then(showResults);
}, 300);

input.addEventListener("input", (e) => search(e.target.value));

// Cancel saat user navigate away atau close modal
modal.addEventListener("close", () => search.cancel());

// Force fire pending call (mis. saat form submit)
form.addEventListener("submit", (e) => {
  search.flush();  // Execute pending search sekarang
  e.preventDefault();
});

Pemakaian dengan leading: true

// Save button — fire pertama langsung, tapi block subsequent click dalam 1 detik
const saveOnce = debounce(saveDocument, 1000, { leading: true });
saveButton.addEventListener("click", saveOnce);

Kapan dipakai

  • Input search live — delay request sampai user berhenti mengetik (~300ms).
  • Auto-save draft — simpan setelah user tidak ketik 2 detik.
  • Resize handler — recalculate layout setelah resize selesai, bukan setiap frame.
  • Scroll handler untuk infinite scroll — load more setelah scroll berhenti.

Catatan

  • Debounce vs throttle: debounce execute setelah pause, throttle execute max 1x per interval. Search box pakai debounce. Scroll metric tracking pakai throttle.
  • this preserved — pakai function declaration (bukan arrow) di debounced supaya this dari caller masuk. Penting kalau di-attach ke React class method atau Vue method.
  • Memory: setiap debounce panggilan create closure baru. Jangan re-create di setiap render — bikin sekali di useMemo atau di luar component.
  • React hook version: convert ke useDebouncedCallback hook kalau pakai React. Atau pakai use-debounce library yang sudah tested.
// React useMemo pattern
const debouncedSearch = useMemo(
  () => debounce(handleSearch, 300),
  []  // empty deps — bikin sekali per mount
);

useEffect(() => {
  return () => debouncedSearch.cancel();  // cleanup
}, [debouncedSearch]);

setTimeout di iOS Safari bisa nge-throttle di background tab (max 1 minute). Tidak masalah untuk debounce 300ms, tapi jangan andalkan untuk timing kritis lebih dari beberapa detik.

# tags

debounceperformanceuivanilla

← Semua snippet Snippet JavaScript lain →