لو فتحت Network tab في DevTools على أي search box بيبعت طلب مع كل حرف، هتلاقي 11 طلب fetch لما تكتب كلمة من 8 حروف في 1.2 ثانية. ده مش UX سيء بس، ده هدر فعلي على CPU العميل، Bandwidth، و database على السيرفر. Debounce بـ 12 سطر JavaScript بيمنع 91% من الطلبات دي بدون ما يبوّظ تجربة المستخدم.
المستوى: متوسط — يفترض إنك تعرف JavaScript أساسي، setTimeout و clearTimeout، وتعاملت قبل كده مع event listeners و closures.
ليه نفس الـ Search Box بيهلك السيرفر
تخيّل صفحة منتجات فيها بحث live. المستخدم بيكتب "iphone 15 pro" — 11 حرف بمتوسط 110ms بين كل ضغطتين. الـ onInput handler بيبعت fetch مع كل ضغطة. النتيجة: 11 طلب بيوصلوا للسيرفر، كل واحد بيعمل database query، 10 منهم نتيجتهم بتترمى لأن المستخدم لسه بيكتب.
المشكلة مش في JavaScript ولا في الـ framework. المشكلة في إن الـ event بيتفعّل بمعدل أعلى من اللي السيرفر بيحتاج يرد عليه. Debounce و Throttle بيحلّوا ده بطريقتين مختلفتين، وكل واحد منهم مناسب لحالة مختلفة. خلط الاتنين أو استخدام الغلط منهم بيخلّيك تحس إنك حليت المشكلة وانت لسه فيها.
قبل التعريف العلمي: مثال زر المصعد وصنبور المياه
تخيّل إن 10 ناس في الـ lobby ضغطوا زر المصعد في نفس الـ 5 ثواني. المصعد مش بيجي 10 مرات. هو بيستنى الكل يخلّص ضغط، وبعد فترة سكون يتحرك مرة واحدة. ده Debounce بالظبط: مهما اتنادت الدالة كذا مرة، هي مش هتنفّذ غير لما تعدّي فترة سكون كاملة بدون استدعاء جديد.
الآن تخيّل صنبور مياه بيدفع كمية ثابتة كل ثانية، حتى لو فتحته على آخره. ده Throttle: الدالة بتتنفّذ بمعدل أقصى محدد، وأي استدعاء بيجي قبل ميعاده بيتأجّل أو يتجاهل. الصنبور مش بيدفع أسرع لو لفّيته أكتر.
التعريف العلمي والفرق الدقيق
من توثيق Lodash ومكتبة Underscore الأصلية اللي عرّفت المفهومين في JavaScript:
- Debounce(fn, wait): لو fn اتنادت 5 مرات خلال wait مللي ثانية، هتتنفّذ مرة واحدة بعد آخر استدعاء بـ wait مللي ثانية. الـ 4 الأولين بيتجاهلوا.
- Throttle(fn, limit): fn بتتنفّذ مرة واحدة كل limit مللي ثانية على الأكثر، مهما اتنادت كذا مرة في الفترة دي.
الفرق العملي اللي بيخلط على ناس كتير:
- Debounce بيأخّر التنفيذ بالكامل، لكن بيضمن إنه يحصل على آخر input بس. مناسب لما "آخر قيمة" هي اللي تهمك.
- Throttle بيتنفّذ بانتظام، لكن ممكن يفقد آخر event لو ما اتعملش بشكل صح. مناسب لما "التحديث الدوري" هو اللي تهم.
كود Debounce شغّال — 12 سطر
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// الاستخدام مع search input
const handleSearch = debounce((query) => {
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(renderResults);
}, 300);
searchInput.addEventListener('input', (e) => handleSearch(e.target.value));