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.
thispreserved — pakaifunctiondeclaration (bukan arrow) didebouncedsupayathisdari 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
useMemoatau di luar component. - React hook version: convert ke
useDebouncedCallbackhook kalau pakai React. Atau pakaiuse-debouncelibrary yang sudah tested.
// React useMemo pattern
const debouncedSearch = useMemo(
() => debounce(handleSearch, 300),
[] // empty deps — bikin sekali per mount
);
useEffect(() => {
return () => debouncedSearch.cancel(); // cleanup
}, [debouncedSearch]);
setTimeoutdi 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