Debounce ve Throttle Ne Zaman Hangi Sorunu Çözer?
Debounce ve throttle aynı sorunu çözmez. Arama kutusunda hangisi işe yarar, scroll handler'da hangisi ters etki üretir, React bileşenlerinde nasıl doğru uygulanır: senaryo bazlı karar rehberi.
Kullanıcı bir şey yaparken -klavyeye basar, sayfayı kaydırır, pencereyi yeniden boyutlandırır- tarayıcı bu eylemleri olabildiğince hızlı bildirir. Sorun şu ki bazı işlemler bu hıza ayak uyduramaz: ağ isteği göndermek, DOM'u yeniden hesaplamak ya da karmaşık bir animasyonu yönetmek. Debounce ve throttle, bu uçurumu kapatan iki ayrı yaklaşımdır; ancak her ikisi de aynı problemi çözmez. Hangisini seçeceğinizi yanlış anlarsanız, birini kullanmak diğerini hiç kullanmamaktan daha kötü sonuç üretebilir.
İkisi de yüksek frekanslı olayları yönetir, ama mekanizmaları farklıdır. Debounce, bir olayın belirli bir süre boyunca tekrar tetiklenmemesini bekler; son tetiklemeden sonra sayacı sıfırlar, o süre dolduğunda işlevi çalıştırır. Throttle ise işlevi belirli bir zaman penceresi içinde en fazla bir kez çalıştırır ve pencere boyunca gelen diğer çağrıları görmezden gelir.
Ayrım teknik değil, kullanım amacına dairdir. Kullanıcının eylemi bittiğinde tepki vermek mi istiyorsunuz? Debounce. Eylem süresince düzenli aralıklarla tepki vermek mi? Throttle. Bu soruyu sormadan seçim yapmak, kodun davranışını tahmin edilemez hale getirir.
Debounce nasıl çalışır: Son tetiklemeden sonra bekleme
Debounce'un mantığını bir senaryo üzerinden anlamak en sağlıklısıdır. Bir arama kutusu düşünün: kullanıcı "javascript framework" yazarken klavyeye 20 kez basar. Debounce olmadan her tuş basışında bir istek gönderilir; debounce ile yalnızca son tuş basışından 300 milisaniye sonra gönderilir. On dokuz istek kaybolur, sadece biri kalır.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
Sayacın sıfırlanması mekanizması, debounce'u "sessizlik bekleyen" bir yapıya dönüştürür. Kullanıcı yazmayı bırakmadıkça işlev hiç çalışmaz. Bekleme süresi çok kısa tutulursa -50 milisaniyenin altı gibi- ardı ardına gelen tuş basışları arasındaki boşluğu yakalayamaz ve her harf isteği yine açılabilir. Çok uzun tutulursa -700 milisaniyenin üstü gibi- kullanıcı tepkisizlik hisseder ve arayüz yavaş görünür.
Bekleme süresi, gerçek sorunun yalnızca bir parçasıdır. Leading ve trailing edge kavramları daha ince bir kontrol katmanı ekler: varsayılan trailing edge davranışında işlev bekleme süresi dolduktan sonra çalışır; leading edge'de ise ilk tetiklemede hemen çalışır, sonraki çağrıları bekleme süresi boyunca yok sayar. Arama kutusunda çoğunlukla trailing edge istenir; ilk harfte istek göndermek genellikle anlamsız sonuç döndürür ve kullanıcı bunu fark etmese de sunucu yükü artar.
Throttle nasıl çalışır: Sabit aralıkta çalışma
Throttle farklı bir şeyi vaat eder: "Bu süre içinde seni en fazla bir kez çağırırım." Scroll gibi saniyede yüzlerce kez tetiklenebilen bir olayı düşünün. Sayfa kaydırılırken sürekli güncellenen bir ilerleme çubuğu veya sticky header pozisyonu, kullanıcı kaydırma bitince değil kaydırma sırasında güncellenmeliyse, throttle doğru seçimdir.
function throttle(fn, interval) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
fn.apply(this, args);
}
};
}
Bu sürüm trailing edge'i tamamen keser; son scroll pozisyonu yakalanmayabilir. Uygulamalarda çoğunlukla hem ilk hem son çağrının çalışması istenir; bu yüzden üretim kütüphanelerindeki throttle uygulamaları bu iki kenarı ayrı ayrı kontrol eder. Sıfırdan yazmak yerine lodash ya da benzer bir utility kütüphanesinin throttle fonksiyonu kullanılıyorsa bu davranış parametrelerle ayarlanabilir.
Aralık değeri, debounce'un delay değeriyle aynı işlevi görmez. 100 milisaniyelik throttle, saniyede en fazla 10 çağrıya izin verir; bu çoğu görsel güncelleme için yeterlidir. Animasyon veya canvas güncellemesi gibi 60 fps hedefleyen senaryolarda throttle yerine requestAnimationFrame tercih edilmelidir, çünkü throttle ekranın yenileme döngüsüyle senkronize değildir.
Arama kutusu: Debounce'un tartışmasız alanı
Debounce ile throttle arasındaki seçimi en net gösteren senaryo arama kutusudur. Kullanıcı yazmayı bitirmeden sonuç göstermek pek anlam taşımaz; her harf değiştiğinde API'ye istek atmak gereksiz ağ trafiği ve sunucu yükü üretir. Debounce burada tam oturur: son tuş basışından 200-400 milisaniye sonra istek gönderilir, yazma süresi boyunca hiç gönderilmez.
Throttle kullanmak ne anlama gelir? Kullanıcı "r", "re", "rea", "reac", "react" yazarken her 300 milisaniyede bir istek gider. Kullanıcı hızlı yazıyorsa debounce gibi davranır, yavaş yazıyorsa her adımda istek açar. Bu davranış tahmin edilemez; hem gereksiz istekler üretir hem de son harfi yakalamakta gecikmeler yaşatabilir.
Form submit düğmesine bağlı debounce leading edge tercih eder. Kullanıcı düğmeye tıklar, istek hemen gider; aynı düğmeye tekrar tıklarsa -ağ gecikmesi sırasında- ikinci istek engellenir. Trailing edge kullanılırsa tıklama ile istek arasında yapay bir gecikme oluşur ve kullanıcı düğmenin çalışmadığını sanabilir.
Scroll ve resize: Throttle'ın doğal ortamı
Scroll ve resize olayları farklı bir ihtiyacı temsil eder. Kullanıcı sayfayı kaydırırken "sticky header aktif olsun mu?" kontrolünü yapmak, kullanıcı kaydırmayı bitirince bir kez yapmakla eş değildir; bekleme anlamını yitirir. Aynı şey resize için de geçerlidir: pencere boyutu değişirken bir layout hesaplaması sürekli güncellenmeli, ama her piksel değişiminde değil.
Scroll throttle'ı 100-150 milisaniyeye ayarlamak çoğu durumda görsel boşlukları önler. Kullanıcı hızlı kaydırırken en kötü senaryo 150 milisaniyelik bir gecikme yaşanmasıdır; bu fark çıplak gözle neredeyse görünmez. Debounce kullansaydınız, kullanıcı sayfayı sürekli kaydırırken sticky header hiç güncellenemezdi ve kaydırma durduğunda tetiklenirdi; bu, bileşenin yanlış pozisyonda kalması demektir.
Resize olayında ise seçim her zaman throttle olmak zorunda değildir. Pencere boyutunu alıp bir hesaplama yapmak istiyorsanız ve bu hesaplama yalnızca nihai boyutu kullanıyorsa, debounce resize bitince bir kez çalışır ve daha verimlidir. Ama hesaplama görsel bir şeyi -chart genişliğini, grid kolonunu- anlık güncelliyorsa, throttle ile sürekli bir güncelleme akışı sağlanır.
Yanlış seçim ters etki üretir
Her ikisini de kullanmamak bazen daha iyi sonuç verir. Tek tıklanması beklenen bir düğmeyi debounce'la sararsanız, kullanıcı çift tıkladığında ikinci tıklama gecikir ve işlev yalnızca bir kez çalışır; bu beklenen davranıştır. Ama bir oyundaki "ateş et" düğmesini debounce'la sararsanız, her tıklamanın bağımsız işlenmesi gerektiğini göz ardı etmiş olursunuz.
Throttle de ters etki üretebilir. Bir analitik olayını her tıklamada kaydetmek istiyorsunuz ve throttle kullanıyorsunuz; hızlı arka arkaya tıklamalar yok sayılır, analitik veriniz eksik kalır. Yine çok bileşenli uygulamalarda render maliyeti söz konusu olduğunda, bir bileşenin state güncellemesini throttle veya debounce ile geciktirmek React'ın render döngüsüyle çakışabilir; beklenmedik UI tutarsızlıkları ortaya çıkabilir.
En sık karşılaşılan hata şudur: scroll üzerinde debounce kullanmak. Kullanıcı kaydırma sırasında görsel geri bildirim almayı beklerken kaydırma duruncaya kadar hiçbir şey güncellenemez. Bu, bir progress bar'ın sayfa boyunca sabit kalmasına, sonra birden doğru pozisyona atlamasına neden olur. useEffect bağımlılık listesi gibi konularda da benzer görünmez hatalar birikerek performans sorunlarına dönüşür.
React bileşenlerinde uygulama farkları
React'ta debounce veya throttle uygulamak, düz JavaScript'ten farklı dikkat gerektirir. En yaygın hata, fonksiyonu doğrudan bileşen gövdesinde oluşturmaktır:
// Her render'da yeni bir debounce örneği oluşur
const handleSearch = debounce((val) => fetchResults(val), 300);
Bileşen her render olduğunda yeni bir debounce sarmalayıcı oluşur; önceki sayacı sıfırlar. useCallback ile sarmalamak bu sorunu çözer, ancak bağımlılık listesini dikkatli yönetmek gerekir. Alternatif olarak useRef içinde saklanabilir:
const debouncedFetch = useRef(
debounce((val) => fetchResults(val), 300)
).current;
Bu yaklaşım bileşen ömrü boyunca aynı debounce örneğini korur. useMemo ve useCallback'in gereksiz hale geldiği durumların tam tersidir bu: burada memoization zorunludur, çünkü her seferinde yeni bir örnek oluşturmak debounce'un tüm amacını ortadan kaldırır.
React ile scroll throttle kullanırken useEffect içinde event listener eklemek ve temizlik fonksiyonunda kaldırmak standart yöntemdir. Throttle fonksiyonunun kendisi de cleanup sırasında flush edilmeli; kütüphane sürümleri genellikle .cancel() veya .flush() metodları sağlar.
İkisini birlikte kullanmak: Karmaşık senaryolar
Bazı durumlar tek başına debounce veya throttle'ı yetersiz bırakır. Otomatik kayıt özelliği bunun iyi örneğidir: kullanıcı yazarken her 2 saniyede bir kaydetmek istiyorsunuz (throttle), ama yazma bittiğinde de hemen kaydetmek istiyorsunuz (debounce). Bu iki davranış çakışır gibi görünür; çözüm ikisini birlikte kullanmaktır.
Bir başka karma senaryo: sonsuz scroll. Sayfa dibine yaklaşıldığında yeni veri yüklemek istersiniz. Throttle ile her 200 milisaniyede scroll pozisyonunu kontrol edersiniz; eşiği geçince yükleme başlatırsınız. Ama aynı yüklemeyi birden fazla tetiklememek için bir "yükleniyor" bayrağı ya da debounce ile son çağrıyı yakalamak gerekir. İki teknik birbirini tamamlar.
Frontend performans checklist'inde de yer aldığı gibi, bu tür kararlar yayına çıkmadan gözden geçirilmesi gereken davranış testlerini kapsar. Çünkü throttle aralığı veya debounce gecikmesi, performansı iyileştirmek yerine kullanıcı deneyimini bozabilir; ve bu tür hatalar çoğunlukla unit testlerde değil, gerçek kullanım sırasında ortaya çıkar.
Ne zaman ikisi de doğru değildir
Bazı sorunlar debounce ya da throttle yerine farklı bir mimari çözüm ister. Klavye kısayollarına bağlı işlemler bunun başında gelir: her tuş basışının bağımsız işlenmesi gerekiyorsa, hiçbir geciktirme mekanizması eklenmemelidir. Benzer şekilde, bileşenler arası koordinasyon gerektiren state güncellemeleri throttle ile geciktirilirse tutarsızlık riski artar.
Bir canvas veya WebGL uygulamasında mouse hareketi izlemek de bu kategoriye girer. Throttle her 100 milisaniyede bir koordinat alır; bu animasyon için çok seyrektir. requestAnimationFrame tabanlı bir döngü, ekranın yenileme hızına kilitlendiğinden hem daha verimli hem de daha doğru koordinat alır.
React.memo'nun gerçekten işe yaradığı durumlar gibi, burada da doğru soruyu sormak tekniği seçmekten önce gelir: "Bu işlev neden bu kadar sık çağrılıyor?" Eğer cevap "bileşen gereksiz yere yeniden render oluyor" ise, throttle bir geçici çözümdür; asıl sorun render optimizasyonunda yatıyor olabilir.
Seçimi pratik olarak netleştirmek için şu iki soruyu kullanabilirsiniz. "Kullanıcının durması ya da bitmesi önemli mi?" - evet ise debounce. "Eylem süresince düzenli güncelleme isteniyor mu?" - evet ise throttle. İkisi de hayırsa, büyük ihtimalle ikisine de gerek yoktur. Teknik borç gibi, yanlış seçilen bir optimizasyon tekniği de ilerleyen dönemde davranış hatalarının biriktiği sessiz bir kaynak haline gelebilir.
Geciktirme süresini belirlemek için spesifik bir evrensel değer yoktur; kullanıcı araştırması ve gözlem gerekir. 300 milisaniye arama kutusu için yaygın bir başlangıç noktasıdır, 100 milisaniye scroll throttle için sık kullanılır; ama bu değerler bağlama göre değişir. Gerçek ölçüm, tahminden her zaman daha güvenilir bir rehberdir.