تقنيات Debouncing و Throttling
المشكلة: كثرة الأحداث السريعة المتتالية
تستجيب تطبيقات الويب الحديثة لمجموعة واسعة من الأحداث التي يطلقها المستخدم: الكتابة في مربع بحث، تغيير حجم نافذة المتصفح، التمرير عبر الصفحة، تحريك الفأرة، أو النقر على الأزرار. كثير من هذه الأحداث تنطلق بمعدل مرتفع للغاية. حركة تمرير واحدة يمكن أن تطلق مئات أحداث التمرير في الثانية. تغيير حجم نافذة المتصفح يطلق حدث resize باستمرار أثناء سحب المستخدم للحافة. الكتابة في حقل بحث تطلق حدث keyup لكل ضغطة مفتاح. إذا كان كل من هذه الأحداث يطلق عملية مكلفة -- مثل استدعاء API أو إعادة حساب DOM معقدة أو حساب ثقيل -- فإن تطبيقك سيتوقف عن العمل بسلاسة. تصبح واجهة المستخدم بطيئة، وتغرق خوادم API بالطلبات المكررة، ويعاني المتصفح من مواكبة عملية العرض.
تأمل هذا السيناريو الواقعي: تبني ميزة البحث أثناء الكتابة. بدون أي تحكم في المعدل، كتابة كلمة "javascript" تطلق 10 طلبات API منفصلة -- واحد لـ "j"، وواحد لـ "ja"، وواحد لـ "jav"، وهكذا. تسعة من هذه الطلبات ضائعة لأن المستخدم لم ينتهِ من الكتابة بعد. يعالج الخادم العشرة جميعها، وتحمل الشبكة العشرة جميعها، ويعالج المتصفح جميع الاستجابات العشر. هذا غير فعال ومكلف ويخلق تجربة مستخدم سيئة مع نتائج متذبذبة.
تقنيتان تحلان هذه المشكلة بأناقة: Debouncing وThrottling. كلتاهما تتحكمان في عدد مرات تنفيذ الدالة استجابة للأحداث السريعة، لكنهما تعملان بطرق مختلفة جوهريا. فهم متى تستخدم كل واحدة هو مهارة حاسمة لبناء تطبيقات ويب عالية الأداء.
Debounce: انتظر حتى تمر العاصفة
يضمن الـ Debouncing أن الدالة تنفذ فقط بعد فترة محددة من عدم النشاط. في كل مرة ينطلق فيها الحدث، يتم إعادة تعيين المؤقت. تعمل الدالة فقط عندما تتوقف الأحداث عن الانطلاق لمدة التأخير. فكر في الأمر مثل باب المصعد: في كل مرة يقترب شخص ما، يبقى الباب مفتوحا ويعيد تعيين مؤقت الإغلاق. يغلق الباب فقط بعد أن لا يقترب أحد لفترة معينة.
المنطق الأساسي بسيط: عندما ينطلق الحدث، امسح أي مؤقت موجود، ثم عيّن مؤقتا جديدا. إذا انطلق الحدث مرة أخرى قبل انتهاء المؤقت، يتم مسح المؤقت القديم ويبدأ مؤقت جديد. تنفذ الدالة فقط عندما يكتمل المؤقت أخيرا دون انقطاع.
تطبيق Debounce الأساسي
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// مسح أي مؤقت تم تعيينه مسبقا
clearTimeout(timeoutId);
// تعيين مؤقت جديد
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// الاستخدام: ينطلق فقط بعد 300 مللي ثانية من توقف المستخدم عن الكتابة
const searchInput = document.getElementById('search');
const handleSearch = debounce(function(event) {
console.log('البحث عن:', event.target.value);
// إجراء استدعاء API هنا
fetchSearchResults(event.target.value);
}, 300);
searchInput.addEventListener('input', handleSearch);
func.apply(this, args) ضروري. فهو يحافظ على سياق this الصحيح ويمرر جميع المعاملات الأصلية إلى الدالة المؤجلة. بدونه، سيضيع سياق معالج الحدث وكائنات الحدث.Debounce البادئ مقابل اللاحق
تطبيق Debounce الأساسي أعلاه هو debounce لاحق -- تنطلق الدالة في النهاية، بعد فترة التأخير من عدم النشاط. لكن أحيانا تريد أن تنطلق الدالة فورا عند الحدث الأول ثم تتجاهل الأحداث اللاحقة حتى تتوقف الموجة. هذا يسمى debounce بادئ (يعرف أيضا بالـ "فوري").
الـ Debounce البادئ مفيد للنقر على الأزرار حيث تريد استجابة فورية عند النقرة الأولى لكنك تريد منع النقرات المزدوجة أو الثلاثية السريعة من تشغيل الإجراء عدة مرات. يحصل المستخدم على استجابة فورية، ويتم تجاهل النقرات اللاحقة ضمن فترة التأخير.
Debounce مع خيارات البادئ واللاحق
function debounce(func, delay, options = {}) {
let timeoutId;
let lastArgs;
let lastThis;
const leading = options.leading || false;
const trailing = options.trailing !== undefined ? options.trailing : true;
return function(...args) {
lastArgs = args;
lastThis = this;
const isFirstCall = !timeoutId;
clearTimeout(timeoutId);
// الحافة البادئة: تنفيذ فوري عند الاستدعاء الأول
if (leading && isFirstCall) {
func.apply(lastThis, lastArgs);
}
timeoutId = setTimeout(() => {
// الحافة اللاحقة: تنفيذ بعد التأخير
if (trailing && lastArgs) {
if (!(leading && isFirstCall)) {
func.apply(lastThis, lastArgs);
}
}
timeoutId = null;
lastArgs = null;
lastThis = null;
}, delay);
};
}
// Debounce لاحق (الافتراضي): ينطلق بعد توقف المستخدم عن الكتابة
const searchHandler = debounce(fetchResults, 300);
// Debounce بادئ: ينطلق فورا، يتجاهل النقرات السريعة
const submitHandler = debounce(submitForm, 1000, { leading: true, trailing: false });
// كلاهما: ينطلق فورا وبعد التأخير
const saveHandler = debounce(saveDraft, 2000, { leading: true, trailing: true });
إضافة طريقة الإلغاء
في التطبيقات الحقيقية، غالبا ما تحتاج إلى إلغاء استدعاء debounce معلق. مثلا، عندما يتم إلغاء تحميل مكون في تطبيق صفحة واحدة، تريد إلغاء أي استدعاءات API معلقة. دالة debounce بجودة إنتاجية يجب أن تكشف طريقة إلغاء.
Debounce مع الإلغاء والتنفيذ الفوري
function debounce(func, delay, options = {}) {
let timeoutId;
let lastArgs;
let lastThis;
const leading = options.leading || false;
const trailing = options.trailing !== undefined ? options.trailing : true;
function debounced(...args) {
lastArgs = args;
lastThis = this;
const isFirstCall = !timeoutId;
clearTimeout(timeoutId);
if (leading && isFirstCall) {
func.apply(lastThis, lastArgs);
}
timeoutId = setTimeout(() => {
if (trailing && !(leading && isFirstCall)) {
func.apply(lastThis, lastArgs);
}
timeoutId = null;
lastArgs = null;
lastThis = null;
}, delay);
}
// إلغاء أي تنفيذ معلق
debounced.cancel = function() {
clearTimeout(timeoutId);
timeoutId = null;
lastArgs = null;
lastThis = null;
};
// تنفيذ الاستدعاء المعلق فورا
debounced.flush = function() {
if (timeoutId) {
clearTimeout(timeoutId);
func.apply(lastThis, lastArgs);
timeoutId = null;
lastArgs = null;
lastThis = null;
}
};
return debounced;
}
// الاستخدام مع التنظيف
const debouncedSave = debounce(saveData, 1000);
// الإلغاء عند إلغاء تحميل المكون أو التنقل
window.addEventListener('beforeunload', () => {
debouncedSave.flush(); // حفظ أي تغييرات معلقة قبل المغادرة
});
// الإلغاء عندما يلغي المستخدم إجراء صريحا
cancelButton.addEventListener('click', () => {
debouncedSave.cancel();
});
Throttle: إيقاع ثابت للتنفيذ
يضمن الـ Throttling أن الدالة تنفذ مرة واحدة على الأكثر خلال فترة زمنية محددة، بغض النظر عن عدد مرات انطلاق الحدث. على عكس الـ Debouncing الذي ينتظر عدم النشاط، يوفر الـ Throttling إيقاعا ثابتا ويمكن التنبؤ به للتنفيذ. الحدث الأول يشغل الدالة، ثم يتم تجاهل الأحداث اللاحقة حتى تمر الفترة الزمنية. بعد انتهاء الفترة، يشغل الحدث التالي الدالة مرة أخرى.
فكر في الـ Throttling مثل مدفع رشاش بمعدل إطلاق ثابت: بغض النظر عن سرعة سحب الزناد، فإنه يطلق فقط بإيقاع محدد. هذا يجعل الـ Throttling مثاليا للأحداث التي تحتاج فيها إلى تحديثات منتظمة أثناء التفاعل المستمر، مثل تتبع موقع التمرير أو تغيير حجم النافذة أو حركة الفأرة.
تطبيق Throttle الأساسي
function throttle(func, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// الاستخدام: تحديث موقع التمرير كل 100 مللي ثانية كحد أقصى
const handleScroll = throttle(function() {
const scrollY = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = Math.round((scrollY / docHeight) * 100);
progressBar.style.width = scrollPercent + '%';
}, 100);
window.addEventListener('scroll', handleScroll);
Throttle مع خيارات البادئ واللاحق
تطبيق Throttle المتين يدعم كلا من التنفيذ البادئ واللاحق. الاستدعاء البادئ ينطلق فورا عند وصول الحدث الأول. الاستدعاء اللاحق يضمن تشغيل الدالة مرة أخيرة بأحدث المعاملات بعد انتهاء موجة الأحداث. هذا يضمن أنك لن تفوت الحالة النهائية أبدا.
Throttle متقدم مع البادئ واللاحق
function throttle(func, limit, options = {}) {
let timeoutId = null;
let lastArgs = null;
let lastThis = null;
let lastCallTime = 0;
const leading = options.leading !== undefined ? options.leading : true;
const trailing = options.trailing !== undefined ? options.trailing : true;
function throttled(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
lastArgs = args;
lastThis = this;
// الحافة البادئة
if (timeSinceLastCall >= limit) {
if (leading) {
lastCallTime = now;
func.apply(lastThis, lastArgs);
} else {
lastCallTime = now;
}
}
// جدولة الحافة اللاحقة
if (trailing && !timeoutId) {
timeoutId = setTimeout(() => {
const timePassed = Date.now() - lastCallTime;
if (timePassed >= limit && lastArgs) {
lastCallTime = Date.now();
func.apply(lastThis, lastArgs);
}
timeoutId = null;
lastArgs = null;
lastThis = null;
}, limit - timeSinceLastCall);
}
}
throttled.cancel = function() {
clearTimeout(timeoutId);
timeoutId = null;
lastArgs = null;
lastThis = null;
lastCallTime = 0;
};
return throttled;
}
// بادئ فقط: ينطلق فورا، يتجاهل اللاحق
const resizeHandler = throttle(recalculateLayout, 200, {
leading: true,
trailing: false
});
// لاحق فقط: ينطلق في نهاية كل فترة
const analyticsHandler = throttle(trackScrollDepth, 500, {
leading: false,
trailing: true
});
// كلاهما (الافتراضي): ينطلق فورا ويلتقط الحدث الأخير
const scrollHandler = throttle(updateParallax, 16, {
leading: true,
trailing: true
});
requestAnimationFrame كأداة Throttle
للتحديثات المرئية مثل الرسوم المتحركة وتأثيرات التمرير وإعادة حسابات التخطيط، يوفر requestAnimationFrame (rAF) آلية throttle طبيعية مرتبطة بمعدل تحديث المتصفح (عادة 60 إطارا في الثانية أو ~16.7 مللي ثانية). استخدام rAF يضمن أن تحديثاتك المرئية متزامنة مع دورة الرسم في المتصفح، وهو أكثر كفاءة من throttle بفاصل زمني ثابت وينتج رسوما متحركة أكثر سلاسة.
نمط Throttle باستخدام requestAnimationFrame
function rafThrottle(func) {
let rafId = null;
let lastArgs = null;
function throttled(...args) {
lastArgs = args;
if (rafId === null) {
rafId = requestAnimationFrame(() => {
func.apply(this, lastArgs);
rafId = null;
lastArgs = null;
});
}
}
throttled.cancel = function() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
lastArgs = null;
}
};
return throttled;
}
// الاستخدام: تأثير تمرير parallax سلس
const parallaxElements = document.querySelectorAll('.parallax');
const updateParallax = rafThrottle(function() {
const scrollY = window.scrollY;
parallaxElements.forEach(el => {
const speed = parseFloat(el.dataset.speed) || 0.5;
const offset = scrollY * speed;
el.style.transform = `translateY(${offset}px)`;
});
});
window.addEventListener('scroll', updateParallax);
// التنظيف عند عدم الحاجة
function cleanup() {
window.removeEventListener('scroll', updateParallax);
updateParallax.cancel();
}
requestAnimationFrame لأي شيء يعدل الخصائص المرئية (التحويلات، الشفافية، الأبعاد). استخدم throttling بالوقت (setTimeout) للمهام غير المرئية مثل تتبع التحليلات أو حفظ البيانات أو استدعاءات API حيث لا يهم التزامن مع الإطارات.Debounce مقابل Throttle: متى تستخدم أيا منهما
الاختيار بين Debouncing و Throttling يعتمد على ما إذا كنت بحاجة إلى الانتظار حتى ينتهي المستخدم من إجراء ما أو ما إذا كنت بحاجة إلى تحديثات منتظمة أثناء إجراء مستمر. إليك دليلا واضحا:
استخدم Debounce عندما: تريد الانتظار حتى يتوقف المستخدم عن أداء إجراء ما قبل التفاعل. تنطلق الدالة مرة واحدة بعد فترة من عدم النشاط. تشمل الأمثلة: الإكمال التلقائي للبحث (انتظر حتى يتوقف المستخدم عن الكتابة)، التحقق من صحة النماذج (تحقق بعد أن ينتهي المستخدم من تحرير حقل)، إعادة حسابات تغيير حجم النافذة (انتظر حتى ينتهي التغيير)، والحفظ التلقائي (احفظ بعد توقف المستخدم عن التحرير).
استخدم Throttle عندما: تحتاج إلى تحديثات دورية متسقة أثناء إجراء مستمر. تنطلق الدالة على فترات منتظمة بغض النظر عن عدد الأحداث. تشمل الأمثلة: تتبع موقع التمرير (تحديث شريط التقدم أثناء تمرير المستخدم)، تتبع حركة الفأرة (تحديث موقع تلميح الأداة)، التمرير اللانهائي (فحص القرب من أسفل الصفحة دوريا)، والتحليلات الفورية (تتبع مقاييس التفاعل على فترات ثابتة).
مقارنة جنبا إلى جنب
// DEBOUNCE: الإكمال التلقائي للبحث
// ينطلق فقط بعد توقف المستخدم عن الكتابة لمدة 400 مللي ثانية
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(async function(event) {
const query = event.target.value.trim();
if (query.length < 2) return;
const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
displayResults(await results.json());
}, 400);
searchInput.addEventListener('input', debouncedSearch);
// THROTTLE: مؤشر تقدم التمرير
// يتم التحديث كل 100 مللي ثانية أثناء التمرير
const progressBar = document.querySelector('.progress');
const throttledScroll = throttle(function() {
const scrolled = window.scrollY;
const total = document.documentElement.scrollHeight - window.innerHeight;
const percent = (scrolled / total) * 100;
progressBar.style.width = percent + '%';
}, 100);
window.addEventListener('scroll', throttledScroll);
// DEBOUNCE: معالج تغيير حجم النافذة
// يعيد الحساب فقط بعد انتهاء المستخدم من تغيير الحجم
const debouncedResize = debounce(function() {
recalculateGrid();
repositionModals();
updateBreakpointClasses();
}, 250);
window.addEventListener('resize', debouncedResize);
// THROTTLE: حركة الفأرة للمؤشر المخصص
// يحدث موقع المؤشر بمعدل 60 إطارا في الثانية
const customCursor = document.querySelector('.cursor');
const throttledMouseMove = rafThrottle(function(event) {
customCursor.style.transform =
`translate(${event.clientX}px, ${event.clientY}px)`;
});
document.addEventListener('mousemove', throttledMouseMove);
مثال واقعي: الإكمال التلقائي للبحث
لنبنِ مكون إكمال تلقائي كامل للبحث يوضح Debouncing في سياق إنتاجي. يتضمن هذا المثال حالات التحميل ومعالجة الأخطاء وتخزين النتائج مؤقتا ودعم التنقل بلوحة المفاتيح.
إكمال تلقائي إنتاجي للبحث مع Debounce
class SearchAutocomplete {
constructor(inputElement, resultsElement, options = {}) {
this.input = inputElement;
this.results = resultsElement;
this.cache = new Map();
this.abortController = null;
this.minLength = options.minLength || 2;
this.delay = options.delay || 350;
// طريقة بحث مؤجلة
this.debouncedFetch = debounce(
this.fetchResults.bind(this),
this.delay
);
this.input.addEventListener('input', this.handleInput.bind(this));
this.input.addEventListener('keydown', this.handleKeydown.bind(this));
}
handleInput(event) {
const query = event.target.value.trim();
if (query.length < this.minLength) {
this.hideResults();
this.debouncedFetch.cancel();
return;
}
// فحص الذاكرة المؤقتة أولا
if (this.cache.has(query)) {
this.displayResults(this.cache.get(query));
return;
}
this.showLoading();
this.debouncedFetch(query);
}
async fetchResults(query) {
// إلغاء أي طلب جارٍ
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal: this.abortController.signal }
);
if (!response.ok) throw new Error('فشل البحث');
const data = await response.json();
this.cache.set(query, data.results);
this.displayResults(data.results);
} catch (error) {
if (error.name !== 'AbortError') {
this.showError('فشل البحث. يرجى المحاولة مرة أخرى.');
}
}
}
showLoading() {
this.results.innerHTML = '<li class="loading">جارٍ البحث...</li>';
this.results.hidden = false;
}
displayResults(items) {
if (items.length === 0) {
this.results.innerHTML = '<li class="empty">لم يتم العثور على نتائج</li>';
} else {
this.results.innerHTML = items
.map(item => `<li role="option">${item.title}</li>`)
.join('');
}
this.results.hidden = false;
}
hideResults() {
this.results.hidden = true;
this.results.innerHTML = '';
}
showError(message) {
this.results.innerHTML = `<li class="error">${message}</li>`;
this.results.hidden = false;
}
handleKeydown(event) {
if (event.key === 'Escape') {
this.hideResults();
this.debouncedFetch.cancel();
}
}
destroy() {
this.debouncedFetch.cancel();
if (this.abortController) {
this.abortController.abort();
}
this.cache.clear();
}
}
// التهيئة
const autocomplete = new SearchAutocomplete(
document.getElementById('search-input'),
document.getElementById('search-results'),
{ delay: 350, minLength: 2 }
);
مثال واقعي: الحفظ التلقائي للنماذج
وظيفة الحفظ التلقائي هي حالة استخدام مثالية لـ Debounce اللاحق. في كل مرة يحرر فيها المستخدم حقلا، تريد حفظ بيانات النموذج -- لكن فقط بعد أن يتوقف لحظة، وليس عند كل ضغطة مفتاح.
الحفظ التلقائي للنموذج مع Debounce
class FormAutosave {
constructor(formElement, saveEndpoint, delay = 2000) {
this.form = formElement;
this.endpoint = saveEndpoint;
this.statusEl = formElement.querySelector('.autosave-status');
this.lastSavedData = null;
this.debouncedSave = debounce(
this.save.bind(this),
delay,
{ leading: false, trailing: true }
);
// الاستماع للتغييرات على جميع حقول النموذج
this.form.addEventListener('input', () => {
this.updateStatus('تغييرات غير محفوظة...');
this.debouncedSave();
});
// التنفيذ الفوري عند مغادرة الصفحة لتجنب فقدان البيانات
window.addEventListener('beforeunload', (event) => {
if (this.hasUnsavedChanges()) {
this.debouncedSave.flush();
event.preventDefault();
}
});
}
async save() {
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
const dataString = JSON.stringify(data);
// تخطي الحفظ إذا لم تتغير البيانات
if (dataString === this.lastSavedData) return;
this.updateStatus('جارٍ الحفظ...');
try {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: dataString
});
if (response.ok) {
this.lastSavedData = dataString;
this.updateStatus('تم حفظ جميع التغييرات');
} else {
this.updateStatus('فشل الحفظ. جارٍ إعادة المحاولة...');
this.debouncedSave();
}
} catch (error) {
this.updateStatus('خطأ في الشبكة. سيتم إعادة المحاولة...');
this.debouncedSave();
}
}
hasUnsavedChanges() {
const current = JSON.stringify(Object.fromEntries(new FormData(this.form)));
return current !== this.lastSavedData;
}
updateStatus(message) {
if (this.statusEl) {
this.statusEl.textContent = message;
}
}
}
// تهيئة الحفظ التلقائي على نموذج
new FormAutosave(
document.getElementById('editor-form'),
'/api/drafts/save',
2000
);
مثال واقعي: منع النقر المزدوج على الأزرار
Debounce البادئ مثالي لمنع إرسال النماذج المكررة أو استدعاءات API المتعددة من النقرات السريعة على الأزرار. النقرة الأولى تنطلق فورا، والنقرات اللاحقة ضمن نافذة التأخير يتم تجاهلها.
منع النقر المزدوج باستخدام Debounce البادئ
const submitButton = document.getElementById('submit-order');
const handleSubmit = debounce(async function(event) {
event.preventDefault();
submitButton.disabled = true;
submitButton.textContent = 'جارٍ المعالجة...';
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(getOrderData())
});
if (response.ok) {
window.location.href = '/order-confirmation';
} else {
throw new Error('فشل الطلب');
}
} catch (error) {
submitButton.disabled = false;
submitButton.textContent = 'تقديم الطلب';
showNotification('فشل الطلب. يرجى المحاولة مرة أخرى.', 'error');
}
}, 1000, { leading: true, trailing: false });
submitButton.addEventListener('click', handleSubmit);
مثال واقعي: تحسين أحداث التمرير
أحداث التمرير هي من بين الأحداث الأعلى تكرارا في المتصفح. بدون Throttling، يمكن لمستمع التمرير الذي يجري قياسات DOM أو حسابات أنماط أن يسبب تأخرا شديدا وفقدان الإطارات.
معالج تمرير محسن مع Throttle
// شريط تقدم القراءة
const readingProgress = document.querySelector('.reading-progress');
const article = document.querySelector('article');
const updateReadingProgress = throttle(function() {
const articleRect = article.getBoundingClientRect();
const articleTop = articleRect.top + window.scrollY;
const articleHeight = articleRect.height;
const windowHeight = window.innerHeight;
const scrollY = window.scrollY;
// حساب مدى تقدم المستخدم في قراءة المقال
const start = articleTop;
const end = articleTop + articleHeight - windowHeight;
const progress = Math.max(0, Math.min(1,
(scrollY - start) / (end - start)
));
readingProgress.style.transform = `scaleX(${progress})`;
readingProgress.setAttribute('aria-valuenow', Math.round(progress * 100));
}, 50);
window.addEventListener('scroll', updateReadingProgress, { passive: true });
// كشف الرأس اللاصق مع Throttle
const header = document.querySelector('.site-header');
let lastScrollY = 0;
const handleHeaderVisibility = throttle(function() {
const currentScrollY = window.scrollY;
if (currentScrollY > lastScrollY && currentScrollY > 100) {
// التمرير لأسفل بعد 100 بكسل: إخفاء الرأس
header.classList.add('header--hidden');
} else {
// التمرير لأعلى: إظهار الرأس
header.classList.remove('header--hidden');
}
lastScrollY = currentScrollY;
}, 100);
window.addEventListener('scroll', handleHeaderVisibility, { passive: true });
{ passive: true } عند إضافة مستمعي أحداث التمرير واللمس التي لا تستدعي preventDefault(). هذا يخبر المتصفح أن المعالج لن يمنع التمرير، مما يسمح له بتحسين أداء التمرير بشكل كبير.مقاييس الأداء والقياس
تقنيات Debouncing و Throttling لا تتعلق فقط بالشعور بالسرعة -- بل تقدم تحسينات أداء قابلة للقياس. يمكنك قياس التأثير من خلال تتبع عدد مرات انطلاق المعالج الأصلي مقابل عدد مرات تنفيذ النسخة المحسنة فعليا.
قياس فعالية Debounce و Throttle
function createMetrics(label) {
let rawCount = 0;
let optimizedCount = 0;
const startTime = Date.now();
return {
trackRaw() { rawCount++; },
trackOptimized() { optimizedCount++; },
report() {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const reduction = rawCount > 0
? ((1 - optimizedCount / rawCount) * 100).toFixed(1)
: 0;
console.log(`--- مقاييس ${label} ---`);
console.log(`المدة: ${elapsed} ثانية`);
console.log(`الأحداث الخام: ${rawCount}`);
console.log(`التنفيذ الفعلي: ${optimizedCount}`);
console.log(`نسبة التخفيض: ${reduction}%`);
console.log(`أحداث في الثانية: ${(rawCount / elapsed).toFixed(1)}`);
}
};
}
// قياس فعالية throttle التمرير
const scrollMetrics = createMetrics('Throttle التمرير');
window.addEventListener('scroll', () => scrollMetrics.trackRaw(), { passive: true });
const measuredScrollHandler = throttle(function() {
scrollMetrics.trackOptimized();
// منطق معالجة التمرير الفعلي هنا
}, 100);
window.addEventListener('scroll', measuredScrollHandler, { passive: true });
// طباعة المقاييس بعد 10 ثوانٍ من التفاعل
setTimeout(() => scrollMetrics.report(), 10000);
// المخرجات النموذجية:
// المدة: 10.0 ثانية
// الأحداث الخام: 847
// التنفيذ الفعلي: 96
// نسبة التخفيض: 88.7%
// أحداث في الثانية: 84.7
في القياسات النموذجية، يقلل throttling لأحداث التمرير عند 100 مللي ثانية من عدد تنفيذ المعالج بنسبة 85-95%. يقلل debouncing لإدخال البحث عند 300 مللي ثانية من استدعاءات API بنسبة 70-90% مقارنة بالإطلاق عند كل ضغطة مفتاح. تترجم هذه التخفيضات مباشرة إلى استخدام أقل للمعالج وطلبات شبكة أقل وتجارب مستخدم أكثر سلاسة.
تمرين عملي
ابنِ صفحة عرض تفاعلية كاملة تعرض كلا من Debouncing و Throttling. أولا، أنشئ حقل بحث يستخدم debounce بتأخير 400 مللي ثانية. اعرض عدادا يوضح عدد ضغطات المفاتيح التي حدثت مقابل عدد استدعاءات API التي تمت (محاكاة باستخدام console.log). ثانيا، أنشئ معالج تغيير حجم النافذة باستخدام debounce يعرض أبعاد النافذة الحالية، لكنه يتحدث فقط بعد 250 مللي ثانية من توقف المستخدم عن تغيير الحجم. ثالثا، أنشئ شريط تقدم مبني على التمرير باستخدام throttle عند 50 مللي ثانية يعرض تقدم القراءة. رابعا، أضف زرا مع debounce بادئ يحاكي إرسال طلب ويعرض حالة التحميل. لكل عرض، اعرض مقاييس فورية توضح عدد الأحداث الخام مقابل عدد التنفيذ المحسن ونسبة التخفيض. هذا التمرين سيعزز فهمك لمتى وكيف تطبق كل تقنية في التطبيقات الإنتاجية.