AbortController: ألغِ طلبات fetch القديمة قبل ما تسبقك
مستوى القارئ: متوسط
هتمنع واجهة البحث من عرض نتائج قديمة، وهتقلل الضغط على الـ API، بدل ما كل حرف يفتح طلب جديد يفضل شغال لحد ما يرجع.
المشكلة باختصار
ركز في سيناريو autocomplete بسيط. المستخدم كتب s ثم se ثم sea بسرعة. الواجهة أرسلت 3 طلبات. الطبيعي إن آخر طلب هو المهم، لكن اللي بيحصل فعلاً إن الطلب الأول ممكن يرجع بعد الثالث بسبب الشبكة أو الكاش أو ضغط السيرفر. النتيجة: المستخدم كتب كلمة أحدث، والواجهة تعرض نتيجة أقدم.
الطريقة الشائعة الغلط هي الاعتماد على debounce فقط. الـ debounce مفيد، لكنه لا يلغي طلبًا خرج بالفعل. لو الطلب وصل للسيرفر، سيظل يستهلك وقتًا وذاكرة واتصالًا. الحل هنا هو AbortController: إشارة إلغاء تمررها إلى fetch، ثم تستدعي abort() قبل إرسال طلب جديد.
مثال بسيط قبل التعريف العلمي
اعتبر إن عندك موظف دعم يرد على آخر رسالة من العميل. العميل بعت 8 رسائل وراء بعض، والمهم هو آخر رسالة فقط. لو الموظف فضل يرد على أول رسالة، الرد هيبقى صحيح لغويًا لكنه غير مناسب للحالة الحالية. AbortController يعمل نفس الفكرة للطلبات غير المهمة: أول ما طلب أحدث يظهر، الطلب القديم يأخذ إشارة توقف.
علميًا، AbortController كائن يولد AbortSignal. هذه الإشارة تمر إلى واجهات غير متزامنة مثل fetch. عند استدعاء abort()، يتم رفض وعد fetch عادة بخطأ اسمه AbortError، وتقدر تتجاهله لأنه إلغاء مقصود وليس فشلًا حقيقيًا.
الحل العملي في JavaScript
الافتراض إن عندك input بحث يضرب endpoint مثل /api/products?q=.... الهدف: طلب واحد نشط فقط لكل مستخدم في نفس خانة البحث.
let searchController = null;
async function searchProducts(query) {
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, {
signal: searchController.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
renderProducts(products);
} catch (error) {
if (error.name === 'AbortError') return;
showSearchError('البحث فشل. جرّب مرة تانية.');
}
}
const input = document.querySelector('#product-search');
input.addEventListener('input', (event) => {
const query = event.target.value.trim();
if (query.length < 2) return;
searchProducts(query);
});لو المستخدم كتب 8 حروف بسرعة، النسخة القديمة قد تترك 8 طلبات نشطة أو معلقة. النسخة دي تجعل الطلبات القديمة ملغاة، ويبقى آخر طلب فقط هو المرشح لتحديث الواجهة. الرقم هنا تقديري مبني على إدخال سريع بدون debounce؛ في تطبيق حقيقي ستقيسه من Network tab أو من عداد active requests في السيرفر.
أضف timeout بدل الانتظار المفتوح
في Node.js الحديث، تقدر تستخدم AbortSignal.timeout(ms) مع بعض APIs المعتمدة على الوعود. في المتصفح، تقدر تعمل timeout يدوي. أفضل طريقة في الواجهات الحساسة هي الجمع بين إلغاء الطلب السابق وحد أقصى للانتظار.
async function fetchWithTimeout(url, ms = 3000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timer);
}
}لو الـ API طبيعيًا يرد خلال 200 إلى 600ms، timeout بقيمة 3000ms معقول. لو عندك بحث بطيء مقصود في أرشيف كبير، 3 ثواني قد تكون عدوانية. الـ trade-off هنا واضح: تكسب واجهة أسرع وطلبات أقل معلقة، لكن ممكن تقطع طلبًا كان سيعود بنتيجة صحيحة بعد زمن أطول.
ما الذي لا يحله AbortController؟
- لا يضمن أن السيرفر أوقف العمل الداخلي فورًا. هو يلغي الطلب من جهة العميل، لكن السيرفر قد يكون بدأ تنفيذ query بالفعل.
- لا يغني عن
debounce. استخدم الاثنين معًا في autocomplete: debounce يقلل عدد الطلبات قبل خروجها، وAbortController يلغي ما خرج وصار قديمًا. - لا يصلح كحل وحيد لمشاكل backend بطيء. لو query نفسها تأخذ 4 ثواني، أصلح الفهرسة أو الكاش قبل تحميل الواجهة مسؤولية المشكلة.
متى لا تستخدم هذه الطريقة
لا تستخدمها مع عمليات لازم تكتمل حتى لو المستخدم غيّر الشاشة، مثل حفظ طلب دفع أو إرسال نموذج مهم. في هذه الحالات الإلغاء من الواجهة قد يربك المستخدم، والأفضل تصميم idempotency واضح في السيرفر. كذلك لا تستخدمها لو كل طلب يمثل عملية audit أو logging يجب تسجيلها كاملة.
مصادر اعتمدت عليها
- MDN: AbortController abort()
- MDN: AbortSignal واستخدامه مع fetch
- Node.js Docs: AbortController وAbortSignal.timeout
الخطوة التالية
افتح أكثر input عندك يرسل طلبات متكررة، واضف عدادًا بسيطًا للطلبات النشطة قبل وبعد AbortController. لو الرقم لا ينزل من أكثر من 3 طلبات إلى طلب واحد غالبًا، راجع مكان حفظ controller ولا تنشئه داخل scope يضيع بين كل input.