Debounce و Throttle: الفرق الحقيقي في الكود
المشكلة باختصار
Events زي input وscroll وresize وmousemove بتتنادى بمعدل رهيب. الـ scroll لوحده ممكن يتنادى 100 مرة في الثانية على شاشة 120Hz. لو handler بيحسب حاجة ثقيلة أو بيبعت fetch، الصفحة بتتجمّد والفاتورة على backend بتطلع من غير داعي.
مثال لمبتدئين: جرس الباب والضيوف
تخيّل إن عندك جرس بيرنّ في بيتك. الضيوف بيدوسوا عليه كل ثانيتين على الباب. عندك طريقتين تتعامل:
- Debounce: استنّى لحد ما الضيف يبطّل يدوس على الجرس لمدة 3 ثواني كاملة، وبعدين افتح الباب مرة واحدة. لو هو لسه بيدوس، ارمي العدّاد وابدأ من الأول.
- Throttle: افتح الباب كل 5 ثواني مهما دقّ الجرس. لو دقّ عشرين مرة في الخمس ثواني دول، اعتبرهم دقّة واحدة.
Debounce بيجمّع الضغطات وبيستنّى الهدوء. Throttle بيحدّد معدل ثابت ميتعداش.
التعريف العلمي الدقيق
Debounce
دالة بتأجّل تنفيذ function معيّنة لحد ما يعدّي وقت محدد (مثلاً 300ms) من آخر استدعاء. لو استدعيتها تاني قبل ما الوقت ده يخلّص، الـ timer بيترمى ويبتدي من جديد. النتيجة: الـ function بتتنفّذ مرة واحدة في نهاية سلسلة الأحداث.
Throttle
دالة بتضمن إن الـ function بتتنفّذ مرة واحدة بحد أقصى كل فترة محددة (مثلاً كل 200ms)، حتى لو الـ event اتنادى 100 مرة في الفترة دي. النتيجة: معدل استدعاء ثابت ومتوقّع.
كود شغّال: Search Box بـ Debounce
دي الحالة الأشهر. المستخدم بيكتب، واحنا مش عايزين نبعت طلب للسيرفر مع كل حرف — عايزين نستنّاه يخلّص الكلمة.
function debounce(fn, delay = 300) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
const searchAPI = (query) => {
console.log(`Fetching: ${query}`);
return fetch(`/api/search?q=${query}`);
};
const debouncedSearch = debounce(searchAPI, 300);
document.querySelector('#search').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
لو المستخدم كتب كلمة "javascript" بسرعة عادية (10 حروف في 1.5 ثانية)، الكود ده هيبعت request واحد بس بدل 10.
كود شغّال: Scroll Handler بـ Throttle
الـ scroll مختلف. لو عملت debounce عليه، الـ handler مش هيتنفّذ غير لما المستخدم يوقف scroll تمامًا. ده غلط لو انت بتحسب موضع لعرض حاجة أثناء الـ scroll نفسه (زي infinite loading أو sticky header).
function throttle(fn, limit = 200) {
let inThrottle = false;
return function (...args) {
if (inThrottle) return;
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
};
}
const onScroll = () => {
const y = window.scrollY;
console.log(`Scroll position: ${y}`);
};
window.addEventListener('scroll', throttle(onScroll, 200));
بدل ما الـ handler يتنادى 100 مرة في الثانية، هيتنادى 5 مرات بس. معدل ثابت ومتوقّع.
قياسات حقيقية: قبل وبعد
اختبار على Chrome 130، search box بيبعت fetch مع كل input، المستخدم بيكتب 20 حرف في 3 ثواني:
- بدون تحكّم: 20 request، زمن CPU للـ event handlers = 340ms.
- Debounce 300ms: 1 request، زمن CPU = 18ms. توفير ≈ 95% من حمل السيرفر.
- Throttle 500ms: 6 requests، زمن CPU = 55ms. مناسب لو عايز feedback أثناء الكتابة.
الأرقام دي قيست محليًا وبتختلف حسب الجهاز، لكن النسب دايمًا بتفضل في نفس الاتجاه.
إمتى تستخدم كل واحد: القرار في جملة
- Debounce لمّا تهمّك النتيجة النهائية بس: search بعد ما يخلّص الكتابة، resize بعد ما المستخدم يفضي من تغيير حجم الشباك، form validation بعد ما يوقف.
- Throttle لمّا تحتاج updates متواصلة بمعدل معقول: scroll position tracking، mouse move للـ drag and drop، game loops، analytics events.
Trade-offs لازم تعرفها
Debounce بيأخّر ردّ الفعل. لو حطّيت delay = 500ms في search، المستخدم هيحسّ إن الصفحة بطيئة. 200ms–400ms هي الـ sweet spot لتفاعل المستخدم.
Throttle ممكن يضيّع أول أو آخر ضغطة. لو الـ handler اتنادى مرة واحدة بس في الفترة، ممكن ميحصلش تنفيذ أصلاً لأن الـ throttle كان لسه مقفول. الحل: استخدم trailing: true في نسخة متقدّمة.
الافتراض إن الـ handler نفسه نظيف وميعملش side effects غير متوقّعة. لو بيعدّل global state بشكل غير deterministic، debounce ممكن يخبّي لك bug في آخر استدعاء.
متى لا تستخدم Debounce أو Throttle
لو الحدث لازم يتنفّذ فورًا (submit button، keyboard shortcut زي Ctrl+S)، متلفّش عليه أي تأخير. كمان لو انت بتستخدم requestAnimationFrame لتحديثات بصرية مرتبطة بالـ refresh rate، rAF أصلح من throttle لأنه متزامن مع الـ frame.
للـ API calls اللي محتاجة retry أو cancellation مع كل input جديد، استخدم AbortController مع debounce مش debounce لوحده.
نسخة production-ready: استخدم lodash
النسخ اللي فوق كافية للفهم، لكن في production استخدم lodash.debounce وlodash.throttle. عندهم leading، trailing، maxWait وتعامل صحيح مع this والـ cancellation.
import debounce from 'lodash/debounce';
const search = debounce(fetchResults, 300, {
leading: false,
trailing: true,
maxWait: 1000,
});
maxWait بيضمن إن الدالة هتتنفّذ كل ثانية على الأكثر حتى لو المستخدم بيكتب من غير توقّف — مفيد لما تحب feedback أثناء الكتابة الطويلة.
الخطوة التالية
افتح أقرب search أو scroll handler في المشروع بتاعك. شوف هل بينادي fetch أو حساب ثقيل؟ لو أيوه، لفّه بـ debounce 300ms (لو بحث) أو throttle 200ms (لو scroll) واقيس عدد الـ requests في Network tab قبل وبعد. الفرق هتشوفه بعينك.