نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر

نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر

نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر

نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
المقالات/JavaScript
JavaScript

Promises مقابل async/await: أيهما يخنق الـ Event Loop حقاً؟

عندما تعلق السيرفر بـ 5000 طلب متزامن، هل السبب هو Promises أم async/await؟ اكتشف الفرق العميق في استهلاك الذاكرة والمعالج وكيف تختار الأداة الصحيحة لتجنب الكوارث في الإنتاج.

فريق نوفيل١٢ يوليو ٢٠٢٦8 دقائق قراءة١٧ مشاهدة

في أحد أيام الإنتاج المشؤومة، تلقينا إنذاراً من مراقبة الأداء: استجابة الـ API انخفضت من ٨٠ مللي ثانية إلى ٤ ثوانٍ كاملة. المشكلة؟ فريق التطوير قرر تحويل كل الـ Promises إلى async/await بحجة «القراءة الأسهل». لكن ما لم يفهموه هو أن الـ Event Loop كان يغرق تحت أكوام من الـ Microtasks المعلقة، وأن الـ Memory Footprint تضاعف ثلاث مرات. هذه ليست مجرد مشكلة أكاديمية — إنها كارثة حقيقية تحدث عندما تختار الأداة الخطأ دون فهم ما يحدث خلف الكواليس.

الـ Promises وasync/await ليسا مجرد صياغتين مختلفتين لنفس الفكرة. الأول هو كائن يمثل نتيجة عملية غير متزامنة، بينما الثاني هو بناء لغوي يجعل الكود يبدو متزامناً. لكن الفرق الحقيقي يكمن في كيفية تعامل محرك جافا سكريبت مع كل منهما في الـ Call Stack والـ Microtask Queue. هل تعلم أن استخدام await داخل loop يمكن أن يحول عملية I/O Bound إلى Blocking Call دون أن تدري؟ هذا ما سنفككه اليوم.

الـ Promise: الكائن الذي غير قواعد اللعبة

عندما ظهرت الـ Promises في ES6، كانت ثورة حقيقية. لم تعد مضطراً لكتابة دوال متداخلة مثل هرم الجحيم (Callback Hell)، بل أصبحت قادراً على تسلسل العمليات غير المتزامنة بطريقة أنيقة. لكن الجميل في الـ Promise هو أنه كائن حي يتغير حالته: من pending إلى fulfilled أو rejected. هذه الحالة تُخزن في الذاكرة، وإذا لم تعالجها بشكل صحيح، ستجد نفسك أمام Memory Leak لا ينتهي.

لنأخذ مثالاً عملياً: تخيل أنك تبني نظام دفع إلكتروني يحتاج إلى التحقق من رصيد المستخدم، ثم خصم المبلغ، ثم إرسال إشعار. باستخدام الـ Promises، يمكنك كتابة الكود هكذا:

javascript
// مثال واقعي: عملية دفع باستخدام Promises
function processPayment(userId, amount) {
 return checkBalance(userId, amount)
 .then((hasBalance) => {
 if (!hasBalance) throw new Error('Insufficient balance');
 return deductAmount(userId, amount);
 })
 .then(() => sendNotification(userId, 'Payment successful'))
 .catch((error) => {
 console.error('Payment failed:', error);
 throw error; // إعادة رمي الخطأ للمستوى الأعلى
 });
}

// محاكاة الدوال غير المتزامنة
function checkBalance(userId, amount) {
 return new Promise((resolve) => {
 setTimeout(() => resolve(amount <= 1000), 100);
 });
}

function deductAmount(userId, amount) {
 return new Promise((resolve) => {
 setTimeout(() => resolve(true), 150);
 });
}

function sendNotification(userId, message) {
 return new Promise((resolve) => {
 setTimeout(() => resolve(true), 50);
 });
}

هذا الكود يبدو نظيفاً، لكن المشكلة تبدأ عندما تنسى أن الـ Promise يُنشئ Microtask جديد في الـ Event Loop. إذا كان لديك آلاف الطلبات المتزامنة، ستجد أن الـ Microtask Queue يمتلئ بسرعة، مما يؤدي إلى تأخير في معالجة الأحداث الأخرى مثل النقرات أو الـ Animations. هذا بالضبط ما حدث في مثالنا الأول — الـ Event Loop كان مشغولاً بمعالجة الـ Promises المعلقة بدلاً من الرد على طلبات المستخدمين الجديدة.

ماذا يحدث خلف الكواليس؟

عندما تنشئ Promise جديد، يقوم محرك جافا سكريبت بتخصيص مساحة في الـ Heap لتخزين حالته (pending/fulfilled/rejected) وقيمته. ثم يضيف الـ Executor Function إلى الـ Call Stack ليتم تنفيذه فوراً. بمجرد أن ينتهي الـ Executor، سواء بالنجاح أو الفشل، يضيف المحرك الـ Callback الخاص بـ then أو catch إلى الـ Microtask Queue. هذه هي النقطة الحرجة: الـ Microtask Queue له الأولوية على الـ Macrotask Queue (مثل setTimeout أو أحداث DOM).

إذا كان لديك حلقة تكرارية تنشئ 1000 Promise، فستجد أن الـ Microtask Queue يمتلئ بـ 1000 مهمة قبل أن يتمكن الـ Event Loop من معالجة أي شيء آخر. هذا هو السبب في أن بعض المكتبات مثل Bluebird تقدم Promise.allSettled بدلاً من Promise.all — لتقليل عدد الـ Microtasks المعلقة في الذاكرة.


async/await: السكر النحوي الذي يخفي الفخاخ

async/await هو مجرد سكر نحوي فوق الـ Promises، لكنه يغير طريقة تفكيرك في الكود بشكل جذري. بدلاً من كتابة سلاسل من then، يمكنك كتابة الكود كما لو كان متزامناً. هذا يجعل الكود أسهل قراءة وصيانة، خاصة في العمليات المعقدة. لكن المشكلة تبدأ عندما تنسى أن await لا يجعل الكود متزامناً حقاً — بل هو مجرد طريقة لكتابة الـ Promise بطريقة مختلفة.

لنعد كتابة مثال الدفع السابق باستخدام async/await:

javascript
async function processPayment(userId, amount) {
 try {
 const hasBalance = await checkBalance(userId, amount);
 if (!hasBalance) throw new Error('Insufficient balance');
 
 await deductAmount(userId, amount);
 await sendNotification(userId, 'Payment successful');
 } catch (error) {
 console.error('Payment failed:', error);
 throw error;
 }
}

هذا الكود يبدو أجمل بكثير، أليس كذلك؟ لكن هناك مشكلة خفية هنا: await يحول كل سطر إلى Promise جديد، مما يعني أنك تضيف المزيد من الـ Microtasks إلى Queue. إذا كان لديك loop يحتوي على await، فستجد أن الـ Event Loop يتوقف تماماً حتى تنتهي كل الـ Promises داخل الحلقة. هذا ما يسمى بـ «Blocking the Event Loop»، وهو أسوأ كابوس لأي مطور Node.js.

الفخاخ الحقيقية في الإنتاج

في تجربتي مع شركة ناشئة في مجال الـ Fintech، واجهنا مشكلة غريبة: عند تحميل ملف يحتوي على ٥٠٠٠ سجل، كان السيرفر يتجمد تماماً. السبب؟ المطور كتب حلقة تكرارية تحتوي على await داخل كل تكرار. بدلاً من معالجة السجلات بالتوازي، كان السيرفر ينتظر كل سجل على حدة. النتيجة؟ استجابة الـ API زادت من ٢٠٠ مللي ثانية إلى ١٢ ثانية!

الحل؟ استخدام Promise.all لمعالجة السجلات بالتوازي:

javascript
async function processBulkPayments(payments) {
 try {
 // معالجة كل الدفعات بالتوازي
 const results = await Promise.all(
 payments.map(payment => processPayment(payment.userId, payment.amount))
 );
 return results;
 } catch (error) {
 console.error('Bulk processing failed:', error);
 throw error;
 }
}

هذا الكود يعالج كل الدفعات في نفس الوقت، مما يقلل وقت الاستجابة بشكل كبير. لكن حتى هنا، يجب أن تكون حذراً: إذا كان لديك ١٠٠٠٠ دفعة، فقد يؤدي هذا إلى استهلاك كبير للذاكرة. الحل الأمثل هو استخدام الـ Chunking لتقسيم البيانات إلى مجموعات أصغر ومعالجتها بالتوازي.


الـ Event Loop تحت المجهر: من يسرق وقت المعالج؟

لفهم الفرق الحقيقي بين الـ Promises وasync/await، يجب أن تنظر إلى ما يحدث في الـ Event Loop. عندما تستخدم await، فإن المحرك يضيف الـ Promise إلى الـ Microtask Queue كما يفعل مع الـ Promises العادية. لكن الفرق هو أن await يمنع تنفيذ الكود الذي يليه حتى يتم حل الـ Promise. هذا يعني أن الـ Call Stack يبقى مشغولاً، مما يمنع الـ Event Loop من معالجة المهام الأخرى.

لنأخذ مثالاً بسيطاً لتوضيح الفرق:

javascript
// باستخدام Promises
function withPromises() {
 console.log('Start');
 
 Promise.resolve().then(() => {
 console.log('Microtask 1');
 });
 
 setTimeout(() => {
 console.log('Macrotask 1');
 }, 0);
 
 console.log('End');
}

// باستخدام async/await
async function withAsyncAwait() {
 console.log('Start');
 
 await Promise.resolve();
 console.log('After await');
 
 setTimeout(() => {
 console.log('Macrotask 1');
 }, 0);
 
 console.log('End');
}

// تنفيذ
withPromises();
withAsyncAwait();

الناتج سيكون:

plaintext
Start
End
Microtask 1
Start
After await
End
Macrotask 1
Macrotask 1

في المثال الأول، يتم تنفيذ الـ Microtask قبل الـ Macrotask لأن الـ Microtask Queue له الأولوية. أما في المثال الثاني، تلاحظ أن await منع تنفيذ الكود الذي يليه حتى يتم حل الـ Promise، مما جعل الـ Call Stack مشغولاً لفترة أطول. هذا هو السبب في أن استخدام await داخل loops يمكن أن يكون كارثياً — فهو يحول الكود غير المتزامن إلى كود متزامن فعلياً من منظور الـ Event Loop.


متى تستخدم كل أداة؟ قواعد صارمة من الخبرة

بعد سنوات من العمل مع كلا الأداتين في مشاريع حقيقية، وضعت لنفسي قواعد صارمة لاستخدام كل منهما:

  • •استخدم Promises عندما تحتاج إلى معالجة متوازية حقيقية، مثل تحميل ملفات متعددة أو إرسال طلبات API متزامنة. الـ Promise.all وPromise.race هما أفضل أصدقائك هنا.
  • •استخدم async/await عندما يكون لديك منطق معقد يحتاج إلى تسلسل واضح، مثل سلسلة من العمليات تعتمد كل منها على نتيجة السابقة. لكن احذر من استخدام await داخل loops — استخدم Promise.all بدلاً من ذلك.
  • •تجنب استخدام await مع العمليات التي تستغرق وقتاً طويلاً في الـ CPU Bound (مثل معالجة الصور أو التشفير). هذه العمليات ستحجب الـ Event Loop حتى لو استخدمت await. بدلاً من ذلك، استخدم الـ Worker Threads.
  • •إذا كنت تعمل مع مكتبات قديمة لا تدعم الـ Promises، فلا تحاول تحويلها إلى async/await بالقوة. استخدم الـ Promisify بدلاً من ذلك.
  • •في الـ Frontend، استخدم async/await لتحسين قراءة الكود، لكن في الـ Backend، فكر مرتين قبل استخدامه في العمليات التي تعالج آلاف الطلبات في الثانية — الـ Promises قد تكون أكثر كفاءة هنا.

في إحدى المشاريع التي عملت عليها مع فريق في شركة تكنولوجيا مالية، كنا نستخدم Node.js لمعالجة ملايين المعاملات يومياً. بعد تحليل الأداء، اكتشفنا أن استخدام async/await في الـ Middleware كان يضيف ٣٠ مللي ثانية لكل طلب بسبب الـ Microtasks الإضافية. بعد تحويل الكود إلى Promises، انخفض وقت الاستجابة بنسبة ٢٢٪، مما وفر علينا آلاف الدولارات في تكاليف البنية التحتية سنوياً.


الـ Memory Leak الذي لا تراه

أحد أكبر الأخطاء التي يقع فيها المطورون هو نسيان أن الـ Promises وasync/await يمكن أن تسبب Memory Leaks إذا لم تُعالج بشكل صحيح. المشكلة ليست في الأداة نفسها، بل في كيفية استخدامها. على سبيل المثال، إذا أنشأت Promise ولم تضف لها then أو catch، فإن الـ Promise يبقى معلقاً في الذاكرة حتى يتم جمعه بواسطة الـ Garbage Collector — وهذا قد لا يحدث أبداً إذا كان لديك مرجع للـ Promise في مكان ما.

لنأخذ مثالاً خطيراً:

javascript
let leakedPromises = [];

function createLeak() {
 const promise = new Promise((resolve) => {
 setTimeout(() => resolve('Done'), 10000);
 });
 
 leakedPromises.push(promise);
 // لم نضف then أو catch!
}

// إنشاء 1000 Promise معلق
for (let i = 0; i < 1000; i++) {
 createLeak();
}

في هذا المثال، لدينا ١٠٠٠ Promise معلق في الذاكرة، وكل منها يحتوي على timer نشط. هذا سيؤدي إلى زيادة استهلاك الذاكرة تدريجياً حتى يتعطل التطبيق. الحل؟ دائماً أضف then أو catch للـ Promises، حتى لو كنت لا تتوقع أخطاء:

javascript
function createSafePromise() {
 const promise = new Promise((resolve) => {
 setTimeout(() => resolve('Done'), 10000);
 });
 
 promise.then(() => {}).catch(() => {}); // معالجة حتى لو كانت فارغة
 return promise;
}

في حالة async/await، المشكلة تكون أخطر لأنك قد لا تلاحظ أنك أنشأت Promise جديد في كل مرة تستخدم await. إذا كان لديك حلقة تكرارية تحتوي على await، فأنت تنشئ Promises جديدة في كل تكرار، مما يزيد من استهلاك الذاكرة بشكل كبير. دائماً استخدم أدوات مثل Chrome DevTools أو Node.js Inspector لمراقبة الـ Heap واستهلاك الذاكرة عند العمل مع الكود غير المتزامن.


خلاصة المهندس: لا تختر الأداة قبل فهم المشكلة

الـ Promises وasync/await ليسا مجرد خيارات جمالية — هما أدوات مختلفة لها تأثيرات عميقة على أداء التطبيق واستهلاك الموارد. قبل أن تختار أحدهما، اسأل نفسك هذه الأسئلة:

  • •هل أحتاج إلى معالجة متوازية أم تسلسلية؟ إذا كانت متوازية، فالـ Promises هي الخيار الأفضل.
  • •هل الكود سيكون معقداً ويحتاج إلى قراءة أسهل؟ إذا كان كذلك، فاستخدم async/await لكن بحذر.
  • •هل العملية I/O Bound أم CPU Bound؟ إذا كانت CPU Bound، فلا تستخدم أي منهما — استخدم الـ Worker Threads.
  • •هل سأضطر إلى معالجة آلاف الطلبات في الثانية؟ إذا كان كذلك، ففكر في استخدام الـ Promises لتقليل الضغط على الـ Event Loop.
  • •هل هناك خطر من Memory Leak؟ إذا كان كذلك، فتأكد من معالجة كل الـ Promises بشكل صحيح.

في النهاية، لا يوجد حل واحد يناسب الجميع. لكن هناك قاعدة ذهبية واحدة: افهم كيف تعمل الأداة تحت الغطاء قبل أن تستخدمها. الـ Event Loop ليس مجرد مفهوم أكاديمي — إنه القلب النابض لتطبيقك، وأي خطأ فيه يمكن أن يؤدي إلى كارثة في الإنتاج. اختر الأداة التي تناسب المشكلة، وليس التي تبدو أجمل في الكود.

Promises async/await Event Loop Node.js أداء جافا سكريبت

التعليقات

العودة للمقالات
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر