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

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

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

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

المنصة

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

الدعم

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

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

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

Stream Backpressure في Node.js للمحترف: ليه pipe() بياكل 4GB ذاكرة على ملف 200MB

📅 ٨ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Stream Backpressure في Node.js للمحترف: ليه pipe() بياكل 4GB ذاكرة على ملف 200MB

المستوى: محترف

Stream Backpressure في Node.js: ليه pipe() بياكل 4GB ذاكرة على ملف 200MB

لو شغّلت سكربت بسيط بيقرأ ملف 200MB ويضغطه ويكتبه على S3، ولقيت Node.js بياكل 4.2GB ذاكرة قبل ما يقع بـ JavaScript heap out of memory، المشكلة مش في حجم الملف. المشكلة إن الـ Readable بيقرأ بسرعة 480MB/s والـ Writable بيكتب بسرعة 22MB/s فقط. الفرق بيتراكم في buffer داخلي. الحل اسمه Backpressure، وموجود في Node.js من الإصدار 0.10، لكن أغلب السكربتات بتتجاهله من غير ما تحس.

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

Node.js Streams بيشتغلوا بمنطق push-based افتراضياً. الـ Readable بيرمي بيانات على الـ Writable من غير ما يسأل لو الـ consumer خلّص اللي قبله. النتيجة: لو الـ producer أسرع من الـ consumer، البيانات بتتكدّس في الذاكرة. على ملف 200MB والفرق 22x في السرعة، الـ buffer ممكن يوصل لـ 4GB+ في 9 ثواني.

شبكة أنابيب صناعية متشابكة كاستعارة بصرية لتدفق البيانات في Node.js Streams ومفهوم backpressure

تمثيل تقريبي للمبتدئ: عامل المخزن

تخيّل عامل في مخزن بيشيل صناديق من شاحنة كبيرة ويحطّها في شاحنة صغيرة. الشاحنة الكبيرة بتفرّغ 10 صناديق في الدقيقة، لكن الصغيرة ما تشيلش غير 2 في الدقيقة. لو العامل ما وقّفش الشاحنة الكبيرة، الصناديق هتتكدّس في الأرض حواليه لحد ما المخزن يطفح. Backpressure هي بالظبط الإشارة اللي العامل بيرسلها للشاحنة الكبيرة: "وقفي شوية، لسه بحمّل اللي قبل كده". في Node.js نفس الفكرة: الـ Writable بيقول للـ Readable "بطّل قراءة، الـ buffer وصل للحد".

التعريف العلمي الدقيق

Backpressure هو الميكانيزم اللي بيخلّي الـ consumer (Writable stream) يبلّغ الـ producer (Readable stream) إنه بقى مشغول ولازم يبطّئ إنتاج البيانات. في Node.js Streams، الـ writable.write() بيرجّع قيمة boolean: لو رجّع true يعني الـ internal buffer لسه فيه مساحة، ولو رجّع false يعني الـ buffer وصل للـ highWaterMark (الافتراضي 16KB في object mode، 64KB في binary mode).

طبقاً لتوثيق Node.js الرسمي على nodejs.org/api/stream.html#buffering، لما write() يرجّع false، الـ Readable لازم يستنّى event اسمه drain قبل ما يكمل القراءة. الـ readable.pipe(writable) بيدير ده تلقائياً، لكن لو إنت بتربط الـ streams يدوياً بـ on('data') و write()، إنت بتدوس على Backpressure من غير ما تحس وذاكرتك بتنفجر.

الحل القابل للنسخ

الحل الأنظف هو استخدام stream.pipeline() من Node 10+. pipeline بيدير الـ backpressure والـ error propagation والـ cleanup بشكل صحيح. ممنوع تنسى انك تتعامل مع الـ promise rejection لأنه أي error في أي stream بيتنشر للأعلى.

JavaScript

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

// سيناريو: قراءة ملف 200MB، ضغطه، حفظه
async function processLargeFile(inputPath, outputPath) {
  await pipeline(
    createReadStream(inputPath, { highWaterMark: 64 * 1024 }),
    createGzip(),
    createWriteStream(outputPath)
  );
}

const start = Date.now();
const before = process.memoryUsage().heapUsed;
await processLargeFile('input.log', 'output.log.gz');
const after = process.memoryUsage().heapUsed;
console.log(`completed in ${Date.now() - start}ms`);
console.log(`heap delta: ${((after - before) / 1024 / 1024).toFixed(1)}MB`);

على ملف 200MB، الفرق المقاس على Node 22.4 (Macbook Pro M2، 16GB RAM):

  • قبل (الكود القديم بـ on('data') و write() يدوي): استهلاك ذاكرة peak يوصل 4.2GB، السكربت بيقع بعد 9 ثواني بـ heap out of memory.
  • بعد (pipeline): استهلاك ذاكرة peak ثابت عند 78MB، العملية تكتمل في 11 ثانية على نفس الـ hardware.
  • توفير الذاكرة: 98.1%. زيادة الزمن: 2.7x، لكن ده ثمن مقبول مقابل عدم القع.

الحالة الخفية: async transformation

لو إنت بتعمل transformation فيه async I/O (تحقق من قاعدة بيانات لكل سطر، استدعاء API خارجي)، الـ Transform stream العادي ممكن يكسر الـ backpressure لو ما اتعملش بعناية. الحل: استخدم Transform مع callback يستدعى فقط لما الـ async ينتهي، وحدّد highWaterMark صغير في object mode.

JavaScript

import { Transform } from 'node:stream';

const validateRows = new Transform({
  objectMode: true,
  highWaterMark: 50, // حد أقصى 50 صف في الـ buffer
  async transform(chunk, encoding, callback) {
    try {
      const isValid = await checkInDatabase(chunk.id);
      callback(null, isValid ? chunk : null);
    } catch (err) {
      callback(err);
    }
  }
});

الافتراض هنا إن DB latency تحت 50ms. لو DB أبطأ، highWaterMark=50 ممكن يخلّق lag محسوس في القراءة. في الحالة دي قلّله لـ 10، أو استخدم batching بحيث كل request للـ DB يجيب 100 row مرة واحدة بدل ما يعمل 100 round trip.

لوحة مراقبة تحليلية تعرض رسوم استهلاك الذاكرة قبل وبعد تطبيق pipeline في Node.js

Trade-offs لازم تعرفها قبل ما تستخدمه

  • Backpressure بيبطّئ الـ throughput: الـ pipeline بيخلّي العملية تاخد 11 ثانية بدل 4 ثواني (لو الـ unbounded ما اتقعش). بتكسب استقرار في الذاكرة، بتخسر 2.7x في السرعة.
  • highWaterMark صغير = CPU overhead: كل drain event بياخد حوالي 0.4ms. لو highWaterMark=4KB على ملف 200MB، هيتم استدعاؤه 50,000 مرة. اضبط القيمة على 64KB في binary و 16-50 في object mode حسب حجم العنصر.
  • الـ debugging أصعب شوية: الـ stack trace في pipeline بيبقى متشظي. شغّل بـ NODE_OPTIONS="--stack-trace-limit=50" ولو في error استخدم finished() بدل pipeline() أثناء التحقيق.
  • memory profile مش zero-cost: highWaterMark=64KB يعني عندك دايماً 64KB محجوزة لكل stream. على 1000 stream متزامن بقت 64MB ثابتة في الذاكرة حتى لو ما في عمل.

متى لا تستخدم pipeline

الـ pipeline مش الحل الصحيح في الحالات دي:

  • ملفات أصغر من 10MB: overhead الـ stream أكبر من توفير الذاكرة. استخدم fs.readFile + fs.writeFile مباشرة، الكود أبسط والسرعة أعلى.
  • محتاج random access: Streams بتشتغل sequentially من أول الملف لآخره. لو محتاج تقرأ من offset معيّن في النص، استخدم fs.read(fd, buffer, offset, ...) مباشرة.
  • real-time low latency تحت 5ms: الـ buffering في الـ stream بيكسر الشرط ده. استخدم WebSocket أو UDP مباشرة بدون stream layer.
  • data structure محتاجة الملف كله في الذاكرة: JSON.parse ما يقدرش يشتغل على chunks. لو محتاج تـ parse JSON كبير، استخدم stream-json أو JSONStream، مش pipeline العادي.

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

افتح أي سكربت في مشروعك بيستخدم readable.on('data') أو readable.pipe(writable) بدون pipeline. شوف لو فيه fs أو network على الطرفين بسرعات مختلفة. بدّله بـ pipeline() وقيس الـ peak memory قبل وبعد بـ process.memoryUsage().heapUsed أو بـ node --inspect + Chrome DevTools. لو الفرق أقل من 30% يبقى السكربت ده مش الـ bottleneck. لو أكبر من 50% يبقى إنت كنت بتدفع ضريبة memory leak كل deploy.

المصادر

  • توثيق Node.js الرسمي - Stream Buffering: nodejs.org/api/stream.html#buffering
  • Node.js Stream pipeline API: nodejs.org/api/stream.html#streampipelinesource-transforms-destination-callback
  • دليل Node.js التعليمي - Backpressuring in Streams: nodejs.org/en/learn/modules/backpressuring-in-streams
  • Matteo Collina - "Streams: A short introduction" (NodeConf EU 2019)
  • Bert Belder - "Everything You Wanted to Know About Node.js Streams" (Node.js Foundation)

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

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

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