Event seperti input, scroll, dan resize bisa terjadi puluhan kali per detik. Kalau tiap event langsung menjalankan kerja berat, misalnya kirim request ke server, halamannya jadi berat dan server-nya kebanjiran. Debounce dan throttle adalah dua cara membatasi seberapa sering sebuah function benar-benar jalan. Bayangin pintu lift: pintunya baru menutup setelah 3 detik gak ada orang masuk, dan tiap ada yang masuk hitungannya mulai lagi. Itu debounce. Sekarang bayangin dispenser yang cuma mengeluarkan satu gelas tiap 3 detik, mau tombolnya kamu tekan berapa kali pun. Itu throttle.
🧪 Tebakan cepat: kolom pencarian memakai debounce 300 ms. Kamu mengetik "sate" dengan jeda 100 ms antar huruf, lalu berhenti. Berapa request yang terkirim: 4, 1, atau 0? Jawabannya ada di bagian "Debounce: tunggu sampai berhenti".
Debounce: tunggu sampai berhenti
Debounce menunda function sampai event-nya berhenti selama delay ms. Tiap panggilan baru membatalkan jadwal yang lama dan memasang jadwal baru.
| Waktu | Kamu mengetik | Yang terjadi |
|---|---|---|
| 0 ms | s | jadwal dipasang: jalan di ms ke-300 |
| 100 ms | sa | jadwal lama dibatalkan, jadwal baru: ms ke-400 |
| 200 ms | sat | dibatalkan lagi, jadwal baru: ms ke-500 |
| 300 ms | sate | dibatalkan lagi, jadwal baru: ms ke-600 |
| 600 ms | (diam) | cari("sate") jalan, request terkirim |
Empat ketikan, 1 request, dengan kata terakhirnya. Itu jawaban tebakannya.
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer); // batalkan jadwal sebelumnya
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const cari = debounce((kata) => {
console.log("fetch /api/search?q=" + kata);
}, 300);
cari("s");
cari("sa");
cari("sat");
cari("sate");
// 300 ms setelah panggilan terakhir, sekali saja:
// fetch /api/search?q=sate
debouncemengembalikan function baru, si pembungkus. Yang kamu panggil tiap event adalah pembungkus ini, bukanfnlangsung.let timerhidup di closure, jadi semua panggilan pembungkus memakaitimeryang sama....argsmengumpulkan semua argumen panggilan jadi array.clearTimeout(timer)membatalkan jadwal yang belum jalan. Kalautimermasihnull, gak terjadi apa-apa.setTimeout(..., delay)memasang jadwal baru dan menyimpan nomornya ditimer.fn.apply(this, args)memanggil function aslinya denganthisdan argumen dari panggilan terakhir.
Tiga hal yang harus benar: sebelum delay lewat, fn belum jalan sama sekali; tiga panggilan beruntun cuma menghasilkan satu kali jalan; dan setelah jalan, pembungkusnya bisa dipakai lagi untuk gelombang berikutnya.
Throttle: paling banyak sekali per interval
Throttle menjalankan function paling banyak sekali tiap interval ms, selama event-nya terus datang. Event di antaranya dibuang.
function throttle(fn, interval) {
let terakhir = 0;
return function (...args) {
const sekarang = Date.now();
if (sekarang - terakhir >= interval) {
terakhir = sekarang;
fn.apply(this, args);
}
};
}
window.addEventListener("scroll", throttle(() => {
console.log("posisi", window.scrollY);
}, 300));
terakhirmenyimpan kapanfnterakhir jalan. Awalnya0, jadi event pertama langsung jalan.- Kalau sudah lewat
intervalsejak terakhir jalan,fndijalankan dan waktunya dicatat. Kalau belum, event itu dilewati.
Scroll terus selama 1 detik memicu event kira-kira tiap 16 ms, sekitar 60 event. Dengan throttle 300 ms, handler jalan di ms ke-0, 304, 608, dan 912: 4 kali, dan tetap jalan berkala selama scroll berlangsung. Debounce 300 ms di situasi yang sama baru jalan sekali, 300 ms setelah scroll berhenti.
Leading dan trailing edge
Edge artinya kapan function jalan dalam satu rombongan event: di awal (leading) atau di akhir (trailing).
- Debounce di atas itu trailing: jalan di akhir, setelah sepi. Versi leading jalan di event pertama, lalu mengabaikan sisanya sampai sepi selama
delay. Cocok untuk tombol yang gak boleh terkirim dua kali. - Throttle di atas itu leading: event pertama langsung jalan. Kekurangannya, posisi terakhir bisa hilang. Di contoh tadi, event setelah ms ke-912 dibuang semua. Throttle dengan trailing menambahkan satu panggilan terakhir dengan nilai terbaru di ms ke-1.212, satu interval setelah panggilan terakhir.
Library seperti Lodash punya opsi { leading, trailing } untuk keduanya.
Pilih yang mana
| Situasi | Pilih | Kenapa |
|---|---|---|
| Kolom pencarian, request ke server | debounce | cukup hasil untuk kata terakhir |
| Simpan draft otomatis | debounce | simpan setelah orangnya berhenti mengetik |
| Scroll yang memanggil API selama scroll berlangsung | throttle | butuh update berkala selama bergerak |
| Resize jendela, lalu hitung ulang layout | debounce | cukup sekali setelah selesai |
| Posisi pointer saat drag | throttle | harus terus mengikuti gerakan |
Patokannya: debounce menunggu aktivitasnya berhenti, throttle membatasi frekuensinya per interval.
⚠️ Jebakan yang sering ketemu
- Membuat debounce baru di tiap event.
input.oninput = (e) => debounce(cari, 300)(e.target.value)bikin pembungkus dantimerbaru tiap ketikan, jadi gak ada yang dibatalkan. Panggildebouncesekali, simpan hasilnya, lalu panggil hasil itu di tiap event. - Lupa
clearTimeout. Tiap panggilan cuma ditunda, gak ada yang dibatalkan, jadi 4 ketikan tetap jadi 4 request. Latihan juga mengecekclearTimeoutada di kodemu. - Menjalankan
fnlangsung. Kalaufndipanggil di dalam pembungkus sebelumsetTimeout, dia jalan di tiap ketikan. Function-nya baru boleh jalan dari dalamsetTimeout. - Argumennya hilang.
setTimeout(fn, delay)memanggilfn()tanpa kata yang dicari. Teruskanargslewatfn.apply(this, args)ataufn(...args).
TL;DR: Debounce menunda function sampai event-nya berhenti selama delay ms: tiap panggilan menjalankan clearTimeout(timer) lalu timer = setTimeout(() => fn.apply(this, args), delay), jadi yang jalan cuma panggilan terakhir dengan argumennya ("sate" diketik 4 huruf, 1 request). Throttle menjalankan function paling banyak sekali per interval selama event terus datang (scroll 1 detik dengan throttle 300 ms jalan 4 kali). Leading berarti jalan di awal rombongan, trailing di akhirnya. Pakai debounce untuk pencarian, draft, dan resize. Pakai throttle untuk scroll dan drag yang butuh update selama bergerak.