ما الفرق بين Debouncing وThrottling في JavaScript؟ ومتى تستخدم كلا منهما؟
تخيل أنك تبحث عن منتج في متجر إلكتروني، وتظهر أمامك اقتراحات للبحث تلقائيا أثناء الكتابة. قد يؤدي كل حرف تكتبه إلى إرسال طلب جديد إلى الخادم. فإذا أدخلت كلمة مكونة من 10 أحرف، فقد يرسل المتصفح 10 طلبات، رغم أن المستخدم قد لا يحتاج إلى النتائج إلا بعد التوقف عن الكتابة للحظة.
وتظهر مشكلة مشابهة عند التعامل مع أحداث التمرير Scroll وتغيير حجم النافذة Resize، إذ قد تتكرر هذه الأحداث مرات عديدة خلال فترة قصيرة. هنا تأتي أهمية تقنيتي Debouncing وThrottling في JavaScript. كلتاهما تساعد على الحد من التنفيذ المتكرر للعمليات، لكن لكل منهما طريقة مختلفة في التحكم بتوقيت التنفيذ.
في هذا المقال، سنتعرف على الفرق بين التقنيتين، ومتى نستخدم كل واحدة منهما، مع أمثلة عملية وأخطاء شائعة ينبغي تجنبها.
ما المقصود بتقنية Debouncing؟
تعتمد تقنية Debouncing على تأجيل تنفيذ العملية حتى تتوقف سلسلة الأحداث لفترة محددة. فمثلا، إذا حددنا مدة الانتظار بـ400 مللي ثانية، فإن كل حدث جديد يعيد بدء المؤقت. ولا تنفذ العملية إلا بعد مرور 400 مللي ثانية دون وقوع حدث جديد.
لنفترض أننا نستخدم هذه التقنية مع مربع البحث:
- يكتب المستخدم الحرف الأول، فيبدأ مؤقت مدته 400 مللي ثانية.
- بعد 150 مللي ثانية، يكتب حرفا آخر، فنلغي المؤقت السابق ونبدأ مؤقتا جديدا.
- يواصل المستخدم الكتابة بسرعة، فتستمر إعادة ضبط المؤقت.
- عندما يتوقف عن الكتابة لمدة 400 مللي ثانية، ننفذ عملية البحث باستخدام القيمة الحالية.
يسمى هذا الأسلوب Trailing Debounce، لأنه ينفذ العملية بعد آخر حدث في السلسلة. وتوجد أساليب أخرى تسمح بالتنفيذ عند بداية السلسلة أو تضع حدا أقصى لمدة التأجيل، لكن التأجيل إلى ما بعد التوقف عن الكتابة من أكثر الاستخدامات شيوعا.
مثال عملي على Debouncing باستخدام JavaScript
let searchTimer;
function onSearch(value) {
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
searchProducts(value);
}, 400);
}
في كل مرة تستدعى فيها الدالة onSearch، تلغي clearTimeout المؤقت السابق، ثم تنشئ setTimeout مؤقتا جديدا. وإذا استمرت الكتابة وكانت المدة بين الأحرف أقل من 400 مللي ثانية، فلن تستدعى searchProducts عند إدخال كل حرف.
لكن هذا لا يضمن إرسال طلب واحد فقط طوال جلسة البحث. فإذا توقف المستخدم لفترة تتجاوز مدة الانتظار ثم استأنف الكتابة، فقد تنفذ عملية البحث أكثر من مرة، وهذا سلوك متوقع.
ما المقصود بتقنية Throttling؟
تعمل تقنية Throttling بطريقة مختلفة. فبدلا من انتظار توقف الأحداث، نسمح بتنفيذ العملية بمعدل محدود أثناء استمرارها.
لنفترض أن لدينا وظيفة تتابع موضع المستخدم أثناء التمرير داخل الصفحة. قد يتكرر حدث Scroll بسرعة كبيرة، ومن غير الضروري تنفيذ عملية مكلفة عند كل حدث. يمكننا بدلا من ذلك السماح بالتنفيذ مرة واحدة على الأكثر كل 200 مللي ثانية.
بهذه الطريقة، يستمر تنفيذ الوظيفة على فترات متباعدة حتى إذا واصل المستخدم التمرير، دون الحاجة إلى انتظار توقفه.
مثال عملي على Throttling باستخدام JavaScript
let lastRun = -Infinity;
function onScroll() {
const now = performance.now();
if (now - lastRun >= 200) {
lastRun = now;
trackScrollPosition();
}
}
window.addEventListener('scroll', onScroll, { passive: true });
يحفظ هذا المثال توقيت آخر تنفيذ، ولا يستدعي trackScrollPosition مرة أخرى إلا بعد مرور 200 مللي ثانية على الأقل. وهو تطبيق بسيط ينفذ أول حدث يسمح به التوقيت ويتجاهل الأحداث الواقعة خلال فترة الانتظار، لكنه لا يضمن تنفيذ الحدث الأخير بعد توقف التمرير.
وإذا كان المطلوب تحديث عنصر مرئي بسلاسة أثناء حركة الصفحة، مثل شريط تقدم القراءة، فقد يكون استخدام requestAnimationFrame مع تقليل العمليات المكلفة أنسب من تحديث الواجهة كل 200 مللي ثانية. لذلك يعتمد الاختيار على طبيعة العملية والنتيجة المطلوبة.
ما الفرق بين Debouncing وThrottling؟
| وجه المقارنة | Debouncing | Throttling |
|---|---|---|
| طريقة التنفيذ | ينتظر فترة هدوء بعد آخر حدث | يسمح بالتنفيذ بمعدل محدود أثناء استمرار الأحداث |
| عند استمرار الأحداث | قد يتأجل التنفيذ باستمرار | يمكن أن يستمر التنفيذ على فترات |
| مثال للاستخدام | البحث التلقائي أثناء الكتابة | تقليل معدل تتبع أحداث التمرير |
| قيد مهم | قد تتأخر النتائج مع الكتابة المتواصلة | قد يتجاهل التطبيق البسيط الحدث الأخير |
باختصار، إذا كنت تحتاج إلى تنفيذ عملية بعد انتهاء المستخدم من سلسلة التفاعلات، ففكر في Debouncing. أما إذا كنت تحتاج إلى تنفيذها بصورة دورية أثناء استمرار التفاعل، فقد تكون Throttling أكثر ملاءمة.
مشكلة مهمة: Debouncing لا تمنع وصول نتائج API بترتيب غير صحيح
لنفترض أن المستخدم كتب lap، فأرسل التطبيق طلب بحث. وبعد لحظات كتب laptop، فأرسل التطبيق طلبا ثانيا. قد تصل استجابة الطلب الثاني أولا، ثم تصل استجابة الطلب القديم لاحقا وتستبدل النتائج الصحيحة بنتائج لم تعد مطابقة لما كتبه المستخدم.
تعرف هذه الحالة باسم Race Condition. ولا تكفي تقنية Debouncing لمعالجتها، لأنها تتحكم في توقيت استدعاء وظيفة البحث، ولا تضمن ترتيب وصول استجابات الشبكة.
من الحلول الممكنة إلغاء الطلب السابق باستخدام AbortController، أو التحقق من أن الاستجابة التي سنعرضها ما زالت تخص أحدث استعلام.
let timer;
let activeController;
function searchLater(query) {
clearTimeout(timer);
if (activeController) activeController.abort();
if (!query.trim()) {
renderResults([]);
return;
}
timer = setTimeout(async function () {
const controller = new AbortController();
activeController = controller;
try {
const url = '/api/products?q=' + encodeURIComponent(query);
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error('Search failed');
const products = await response.json();
if (!controller.signal.aborted) {
renderResults(products);
}
} catch (error) {
if (error.name !== 'AbortError') {
showSearchError(error);
}
} finally {
if (activeController === controller) {
activeController = null;
}
}
}, 400);
}
يفترض المثال وجود الدالتين renderResults وshowSearchError ضمن التطبيق. ومن المهم ملاحظة أن إلغاء الطلب في المتصفح لا يعني بالضرورة توقف الخادم عن معالجته، لكنه يساعد على منع استجابة قديمة من تحديث واجهة المستخدم بعد استجابة أحدث.
متى يكون استخدام Debouncing أو Throttling غير مناسب؟
من الأخطاء الشائعة تطبيق إحدى التقنيتين على كل حدث متكرر دون النظر إلى السلوك المطلوب:
- اقتراحات البحث التلقائية: غالبا ما تكون Debouncing مناسبة للبحث أثناء الكتابة، لكن يجب الموازنة بين تقليل الطلبات وسرعة ظهور الاقتراحات، خاصة إذا كان المستخدم يتوقع استجابة فورية.
- تحديث العناصر المرئية أثناء التمرير: قد تمنع Debouncing التحديث طوال استمرار التمرير، بينما قد يؤدي استخدام Throttling بفاصل زمني كبير إلى حركة متقطعة.
- عمليات الدفع والحفظ: لا يكفي التحكم في ضغطات المستخدم من الواجهة الأمامية لمنع تنفيذ العملية مرتين. تحتاج العمليات الحساسة إلى ضوابط على الخادم، وقد تستفيد من
Idempotency Keysوفق طبيعة العملية. - العمليات ذات التكلفة المرتفعة: تقليل مرات التنفيذ لا يعالج خوارزمية بطيئة أو استعلام قاعدة بيانات غير محسن. من الضروري قياس تكلفة العملية نفسها وتحسينها.
هل تحسن Debouncing وThrottling أداء التطبيقات فعلا؟
يمكن للتقنيتين تحسين الأداء عندما تمنعان عمليات متكررة لا حاجة إليها، سواء داخل المتصفح أو عند التواصل مع الخادم. لكن التحسن ليس مضمونا في جميع الحالات. فقد يؤدي تأخير التنفيذ أكثر من اللازم إلى جعل التطبيق يبدو بطيئا للمستخدم حتى إن انخفض عدد طلبات الشبكة.
لذلك، يفضل استخدام أدوات المطور في المتصفح DevTools لقياس عدد الطلبات وأوقات الاستجابة ومقارنة السلوك قبل التعديل وبعده. وينبغي اختبار سيناريوهات مثل الكتابة البطيئة، ومسح مربع البحث، وبطء الاتصال، وتغيير الاستعلام قبل وصول النتائج.
أسئلة شائعة
هل تمنع Debouncing جميع طلبات البحث المتكررة؟
لا. إنها تجمع الأحداث المتقاربة زمنيا، لكن توقف المستخدم لمدة تتجاوز فترة الانتظار قد يؤدي إلى تنفيذ عملية بحث جديدة.
هل تؤخر Throttling كل حدث ثم تنفذه لاحقا؟
ليس بالضرورة. فهي تحدد معدل التنفيذ، وقد تتجاهل بعض التطبيقات الأحداث الواقعة داخل فترة الانتظار، بينما تحتفظ تطبيقات أخرى بتنفيذ أخير بعد انتهاء الفترة.
هل نحتاج إلى مكتبة خارجية لتنفيذ التقنيتين؟
لا. يمكن تنفيذ النماذج البسيطة باستخدام setTimeout وclearTimeout وperformance.now. لكن المكتبات المتخصصة قد توفر خيارات إضافية مثل leading وtrailing وmaxWait.
الخلاصة
الفرق الجوهري بين Debouncing وThrottling لا يتعلق بعدد أسطر الشيفرة، بل بتوقيت تنفيذ العملية. تنتظر Debouncing توقف الأحداث لفترة محددة، بينما تسمح Throttling بالتنفيذ بمعدل محدود أثناء استمرارها.
قبل اختيار إحداهما، اسأل نفسك: هل أحتاج إلى النتيجة النهائية بعد توقف المستخدم عن التفاعل، أم أحتاج إلى تحديثات متتابعة أثناء التفاعل؟ وإذا كانت العملية تتضمن طلبات شبكة، فلا تنس التعامل مع الطلبات القديمة وترتيب وصول الاستجابات. فالهدف ليس تقليل عدد الطلبات فحسب، بل تنفيذ العمل المناسب في الوقت المناسب دون التأثير سلبا على تجربة المستخدم.