أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Backpressure في Node.js: عالج 2GB بدون تفجير الذاكرة

📅 ٢٦ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Backpressure في Node.js: عالج 2GB بدون تفجير الذاكرة

Backpressure في Node.js: عالج 2GB بدون تفجير الذاكرة

مستوى القارئ: متوسط

هتكسب من المقال ده إنك تعرف تعالج ملفات كبيرة أو streams بطيئة في Node.js بدون ما الذاكرة تطلع لحد OOM. ركز: المشكلة غالبًا مش إن السيرفر ضعيف، المشكلة إن الكود بيسمح للبيانات تتراكم أسرع من استهلاكها.

المشكلة باختصار

عندك سكربت بياخد ملف log حجمه 2GB، يفلتر السطور المهمة، وبعدها يكتب النتيجة في ملف تاني أو يبعتها لخدمة HTTP. الطريقة السهلة هي fs.readFile، لكنها بتحمّل الملف كله في الذاكرة. الطريقة اللي شكلها أذكى هي إنك تستخدم stream، لكن لو كتبت chunks بسرعة في وجهة أبطأ بدون احترام drain، أنت عملت queue كبير في الذاكرة بدل ما حلّيت المشكلة.

اللي بيحصل فعلاً: الـ Readable بيطلع بيانات بسرعة، والـ Writable مش قادر يكتب بنفس السرعة. لو تجاهلت الإشارة دي، الذاكرة تزيد بهدوء. في اختبار عملي على ملف 2GB، readFile ممكن يوصل لاستهلاك قريب من 2100MB، وكتابة stream يدوية غلط ممكن تقف عند 820MB، بينما pipeline مع backpressure يحافظ على حدود حوالي 74MB. الأرقام تقديرية لاختبار واحد، لكنها توضح الاتجاه.

مخطط يوضح تدفق Node.js Stream بين منتج سريع ومستهلك بطيء مع إشارة backpressure

مثال بسيط قبل التعريف العلمي

اعتبر عندك موظف بيجهز طلبات بسرعة، وموظف تاني بيلفّها في كراتين ببطء. لو الأول فضل يرمي الطلبات على الترابيزة، المكان هيمتلئ. الحل مش إننا نطلب من الموظف التاني يسرع دائمًا. الحل إن الأول يقف لحظة لما الترابيزة توصل للحد المسموح، ويرجع يشتغل لما المساحة تفضى.

ده بالظبط backpressure. في Node.js، الـ buffer هو الترابيزة، وhighWaterMark هو الحد اللي بعده Node يقول: استنى. لما writable.write(chunk) ترجع false، دي إشارة إن الوجهة مش جاهزة للمزيد. لما يحصل حدث drain، تقدر تكمل.

أفضل طريقة: استخدم pipeline لما تقدر

لو عندك سلسلة واضحة: قراءة ملف، Transform، كتابة ملف أو ضغطه، استخدم stream/promises وpipeline. المكسب: backpressure وإدارة أخطاء وتنظيف موارد في مسار واحد. الـ trade-off هنا إن الكود أقل مرونة من التحكم اليدوي الكامل، لكنه أفضل لمعظم الحالات الإنتاجية.

JavaScript
import { createReadStream, createWriteStream } from 'node:fs';
import { Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';

const onlyErrors = new Transform({
  transform(chunk, encoding, callback) {
    const lines = chunk.toString('utf8').split('\n');
    const filtered = lines.filter(line => line.includes('ERROR')).join('\n');
    callback(null, filtered ? filtered + '\n' : '');
  }
});

await pipeline(
  createReadStream('app.log', { highWaterMark: 64 * 1024 }),
  onlyErrors,
  createWriteStream('errors.log')
);

console.log('done');

الافتراض إنك بتتعامل مع text logs أو ملفات كبيرة لا تحتاج تحميلها كلها مرة واحدة. لو كل خطوة محتاجة الملف كاملًا في الذاكرة، مثل تحليل ZIP كامل أو بناء index عالمي، stream وحده مش هيحل التصميم.

لو لازم تكتب يدويًا: احترم false وdrain

فيه كود شائع غلط: تعمل loop على chunks وتستدعي write بدون انتظار. الطريقة دي بتفشل لما الوجهة أبطأ من المصدر. البديل إنك توقف لما write ترجع false.

JavaScript
import { once } from 'node:events';

async function writeSafely(readable, writable) {
  for await (const chunk of readable) {
    if (!writable.write(chunk)) {
      await once(writable, 'drain');
    }
  }
  writable.end();
  await once(writable, 'finish');
}

هنا أنت بتقبل تكلفة انتظار صغيرة مقابل منع تراكم غير محدود. لو عندك 20 request في نفس اللحظة، الفرق يظهر بسرعة. بدل ما كل request يحجز مئات الميجابايت، كل stream يفضل قريب من حجم الـ buffers الداخلية.

مخطط أعمدة يقارن استهلاك الذاكرة بين readFile وstream بدون انتظار drain وpipeline مع backpressure

إزاي تقيس إن الحل نجح

متقيسش بالإحساس. شغّل نفس الملف قبل وبعد، وسجل peak RSS. مثال سريع:

JavaScript
setInterval(() => {
  const rssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
  console.log('rss_mb=', rssMb);
}, 1000).unref();

لو الملف 2GB والذاكرة فضلت تحت 100MB أو 150MB مع اختلاف بسيط حسب التحويل، أنت غالبًا ماشي صح. لو الذاكرة بتزيد خطيًا مع حجم الملف، عندك تخزين غير مقصود: array بتجمع chunks، transform بيحوّل كل chunk إلى string ضخم، أو write لا ينتظر drain.

trade-offs وما يجب الانتباه له

  • pipeline يكسبك أمان وسهولة، لكنه يجعل السلسلة خطية. لو عندك branching معقد، قد تحتاج تصميم أوضح.
  • highWaterMark أكبر يقلل عدد مرات التوقف أحيانًا، لكنه يزيد الذاكرة لكل stream. لو عندك 200 اتصال متزامن، 1MB لكل stream يعني 200MB قبل البيانات الفعلية.
  • Transform بسيط ممتاز للفلترة والتحويل. لكن لو التحويل يحتاج انتظار API خارجي، لازم تتحكم في concurrency بدل ما تطلق طلب لكل chunk.

متى لا تستخدم هذه الطريقة

لا تستخدم Streams كحل سحري لو المعالجة نفسها تحتاج كل البيانات مرة واحدة. مثال: ترتيب ملف كامل عالميًا، أو حساب checksum لملف مضغوط بعد فكّه كاملًا في الذاكرة داخل مكتبة لا تدعم streaming. كذلك لا ترفع highWaterMark عشوائيًا لو المشكلة الأساسية هي API بطيء أو disk write بطيء. عالج عنق الزجاجة بدل ما تكبر الطابور.

مصادر اعتمدت عليها

  • Node.js Learn: Backpressuring in Streams
  • Node.js Stream API Documentation
  • Node.js File System API Documentation

الخطوة التالية

افتح أي كود عندك بيستخدم fs.readFile مع ملف ممكن يكبر، وحوّله إلى pipeline. بعد التحويل، شغّل نفس الملف وسجل rss_mb كل ثانية. لو الذاكرة ما نزلتش، فتّش عن array أو string بتجمع البيانات داخل Transform.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة