Debounce vs Throttle: القرار بالظبط
المشكلة باختصار
أحداث زي input وscroll وresize بتتطلق بمعدل عالي جدًا. الـ scroll ممكن يطلق أكتر من 100 حدث في الثانية على متصفح عادي. لو ربطت بيه دالة تعمل حسابات أو ترسم عناصر DOM، الصفحة هتتلج. ولو ربطت بيه طلب API، هتحرق الـ rate limit في ثواني.
الحل مش إنك تمنع الحدث. الحل إنك تتحكم في معدل استدعاء الدالة المرتبطة بيه. هنا بييجي دور Debounce و Throttle.
Debounce: استنى لحد ما يهدا
Debounce بيقولك "نفذ الدالة بس لما يعدي وقت معين من غير ما يتكرر الحدث". لو المستخدم لسه بيكتب، الـ timer بيتعمل reset. الدالة بتشتغل مرة واحدة بعد ما يبطل.
الاستخدام الأشهر: حقل بحث يضرب API. مش منطقي تبعت طلب مع كل حرف. منطقي تستنى 300ms بعد آخر حرف، وبعدين تبعت طلب واحد بالنص الكامل.
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// الاستخدام الفعلي
const searchInput = document.getElementById("search");
const handleSearch = debounce(async (value) => {
const res = await fetch(`/api/search?q=${encodeURIComponent(value)}`);
const data = await res.json();
renderResults(data);
}, 300);
searchInput.addEventListener("input", (e) => handleSearch(e.target.value));
النتيجة القابلة للقياس: مستخدم بيكتب كلمة من 10 حروف في ثانية ونص. بدون debounce: 10 طلبات HTTP. مع debounce(300ms): طلب واحد فقط. توفير 90% من الطلبات، والـ backend بياخد نفس.
Throttle: نفذ بانتظام مش أكتر من كدا
Throttle بيختلف بالظبط في النية. هو بيقولك "نفذ الدالة بحد أقصى مرة كل X ملي ثانية، حتى لو الحدث اتطلق 1000 مرة في الفترة دي". مش بيستنى السكون، بيفرض إيقاع ثابت.
الاستخدام الأشهر: scroll handler يحدث موقع عنصر في الصفحة، أو يحسب لو المستخدم وصل لآخر الـ list عشان يعمل pagination. لو استنيت السكون (debounce)، المستخدم هيوصل آخر الصفحة ومفيش حاجة هتحصل لحد ما يبطل scroll. ده سلوك غلط.
function throttle(fn, limit) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn.apply(this, args);
}
};
}
// الاستخدام الفعلي
const handleScroll = throttle(() => {
const scrollPercent =
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
updateProgressBar(scrollPercent);
}, 100);
window.addEventListener("scroll", handleScroll);
النتيجة: بدلًا من 100+ استدعاء في الثانية، الدالة بتشتغل 10 مرات فقط (مرة كل 100ms). الـ UI بيفضل سلس والحسابات بتقل بنسبة ~90%.
الفرق اللي بيكسر الـ production
الناس بتستخدم debounce في مكان throttle والعكس. النتيجة: bug صعب تشخيصه.
- Debounce: الدالة بتشتغل مرة واحدة بعد آخر حدث. لو الأحداث ما بطلتش، الدالة ما بتشتغلش أبدًا. مناسب: بحث، auto-save، validation بعد الكتابة.
- Throttle: الدالة بتشتغل بانتظام أثناء الأحداث. لو الأحداث مستمرة، الدالة بتتنفذ كل X ms. مناسب: scroll، mousemove، resize، game loops.
مثال كارثي حصل في projects فعلية: استخدام debounce على زرار "إرسال" عشان تمنع double-click. المستخدم ضغط مرتين بسرعة، الدالة بتتأجل، الطلب بيتبعت بعد 300ms، المستخدم يفتكر إن الزرار بايظ ويضغط تاني. الحل الصح هنا هو throttle مع leading edge (ينفذ أول مرة فورًا، ويمنع التكرار لفترة).
trade-offs لازم تعرفها
كل واحد منهم معاه ثمن.
- Debounce بيأخر استجابة المستخدم. 300ms بتحس بيها في البحث الحي. لو عايزها فورية استخدم 100–150ms مع إشارة loading.
- Throttle بيضيع آخر حدث. لو المستخدم وقف scroll في نص فترة throttle، آخر موضع ممكن ميتسجلش. الحل: استخدم نسخة فيها
trailing: trueزي اللي في lodash. - الدالتين محتاجين cleanup. في React مع
useEffect، لازم ترجعclearTimeoutفي الـ cleanup function، وإلا هتلاقي memory leaks في components بتتعمل unmount.
افتراض مهم: الأرقام (300ms, 100ms) مبنية على أن المستخدم بيتعامل مع الـ UI على جهاز عادي. لو التطبيق على mobile بشبكة 3G، ممكن تحتاج 500ms للـ debounce عشان تقلل الطلبات أكتر.
متى لا تستخدم أيهما
في حالات الحل فيها مش debounce ولا throttle:
- طلب لازم يتبعت فورًا: زي دفع أو تأكيد عملية. هنا استخدم disable للزرار + loading state بدل ما تأجل الطلب.
- الحدث بيحصل مرة واحدة فعلًا: زي
submitأوclickعلى زرار عادي. مفيش داعي لأي من الاتنين. - عندك Observable (RxJS): استخدم
debounceTimeأوthrottleTimeمن الـ library مباشرة، بدل ما تكتب implementation يدوي.
جدول قرار سريع
- بحث حي يضرب API → debounce 300ms
- حفظ تلقائي لنص طويل → debounce 1000ms
- scroll يحدث progress bar → throttle 100ms
- mousemove لرسم في canvas → throttle 16ms (60fps)
- resize يحدث layout → throttle 150ms
- button anti-double-click → throttle 500ms leading
الخطوة التالية
افتح devtools network tab في التطبيق بتاعك وفلتر على XHR. ابحث في حقل البحث واعد الطلبات. لو لقيت أكتر من طلب في الثانية، ضيف debounce 300ms على الـ handler. لو عندك scroll handler، شغل Performance tab في Chrome واتفرج على عدد استدعاءات الدالة في الثانية. أي رقم فوق 10 معناه إنك محتاج throttle. استخدم lodash.debounce وlodash.throttle مباشرة لو مش عايز تكتب implementation بنفسك — مجرّبين في production لآلاف المشاريع.