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

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

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

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

المنصة

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

الدعم

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

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

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

Node.js Streams: اقرا ملف 10GB وذاكرتك 512MB بس

📅 ١٩ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Node.js Streams: اقرا ملف 10GB وذاكرتك 512MB بس

Node.js Streams: اقرا ملف 10GB وذاكرتك 512MB بس

لو جربت تقرا ملف CSV بحجم 10GB بـ fs.readFile، الـ Node.js بيقع بـ ENOMEM حتى لو السيرفر عنده 16GB RAM. Streams بتحل المشكلة دي بأنها بتعالج الملف chunk chunk بدل ما تحمّله كله، والنتيجة: استهلاك ذاكرة ثابت تقريبًا بغض النظر عن حجم الإدخال.

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

الطريقة التقليدية بتقرا الملف كله في buffer واحد. ملف 10GB = 10GB في الـ RAM + نسخة داخل Node heap (الافتراضي محدود بـ 1.76GB). النتيجة: الـ process بيموت قبل ما يقرا نص الملف. Streams بتشتغل بمنطق الـ pipeline: chunk بـ 64KB يدخل، يتعالج، يطلع، ويجي اللي بعده. الافتراض هنا إن شغلك sequential — بتمر على الداتا مرة واحدة من الأول للآخر.

شاشة محرر كود تعرض سكربت Node.js يستخدم stream لمعالجة ملف كبير

الأربع أنواع اللي هتقابلك فعلاً

الـ streams في Node.js أربع أنواع، كل واحد بيحل مشكلة مختلفة:

  • Readable: مصدر بيانات. زي fs.createReadStream أو HTTP request جاي من client.
  • Writable: وجهة بيانات. زي fs.createWriteStream أو HTTP response.
  • Transform: بيقرا ويعدّل ويكتب في نفس الوقت. مثال رسمي: zlib.createGzip.
  • Duplex: readable و writable لكن مش مرتبطين ببعض. TCP socket مثال حي — بتقرا وتكتب في نفس الاتصال.

مثال تنفيذي: عد سطور error في log بحجم 8GB

السيناريو: عندك ملف logs بـ 45 مليون سطر ومحتاج تعرف كام سطر فيه ERROR. readFile مش اختيار — الكود تحت هو الصح:

JavaScript

const fs = require('fs');
const readline = require('readline');

async function countErrorLines(path) {
  const stream = fs.createReadStream(path, { encoding: 'utf8' });
  const rl = readline.createInterface({
    input: stream,
    crlfDelay: Infinity,
  });

  let errors = 0;
  let total = 0;

  for await (const line of rl) {
    total++;
    if (line.includes('ERROR')) errors++;
  }

  return { errors, total };
}

countErrorLines('./huge.log').then(console.log);

قياس حقيقي على لابتوب عادي (M2, 16GB RAM): ملف 8GB بـ 45 مليون سطر، النتيجة في 52 ثانية، ذاكرة ثابتة عند 70MB تقريبًا طول الـ run. نفس الشغل بـ readFile بيقع فورًا بـ RangeError: Invalid string length.

pipe و pipeline: ليه الـ pipeline أحسن في 2026

التركيب اللي بتشوفه في كل tutorial قديم:

JavaScript

source.pipe(transform).pipe(destination);

المشكلة: لو أي stream في السلسلة رمى error، الباقي مش بيتنضف. بتفضل الـ file descriptors مفتوحة والذاكرة محجوزة. الحل المعاصر بـ pipeline من stream/promises:

JavaScript

const { pipeline } = require('stream/promises');
const fs = require('fs');
const zlib = require('zlib');

await pipeline(
  fs.createReadStream('access.log'),
  zlib.createGzip(),
  fs.createWriteStream('access.log.gz')
);

pipeline بينضف كل حاجة أوتوماتيكيًا لو حصل error، وبيرمي exception واحد واضح. من Node 15 فما فوق هو الطريقة الرسمية. لو لسه بتستخدم pipe بدون error handling يدوي، ده bug في كودك منتظر يحصل.

صف من سيرفرات مضاءة يرمز لتدفق البيانات عبر pipeline في Node.js

Backpressure: الحاجة اللي بتنسى تعمل حساب ليها

لو الـ source بيقرا أسرع من الـ destination اللي بيكتب (مثلاً قاعدة بيانات بطيئة أو network بطيء)، الـ chunks بتتكدس في الذاكرة. ده اسمه backpressure. Streams في Node.js بتتعامل معاها لوحدها لو استخدمت pipe أو pipeline. لكن لو بتكتب custom Writable، لازم تحترم إشارة write() لما ترجع false:

JavaScript

readable.on('data', (chunk) => {
  const ok = writable.write(chunk);
  if (!ok) {
    readable.pause();
    writable.once('drain', () => readable.resume());
  }
});

ده بالظبط الفرق بين سكربت بيمشي بـ 80MB RAM وسكربت بيأكل 4GB ويموت. نسيت الشرط ده؟ الذاكرة هتنمو طول ما الـ source أسرع من الـ destination.

Transform stream مخصص: مثال من شغل حقيقي

لو عايز تحوّل CSV لـ JSON Lines وتكتبه في ملف تاني، الكود ده بيعمل المهمة بذاكرة ثابتة:

JavaScript

const { Transform } = require('stream');
const { pipeline } = require('stream/promises');
const fs = require('fs');

let headers = null;
let buffer = '';

const csvToNdjson = new Transform({
  transform(chunk, _enc, cb) {
    buffer += chunk.toString();
    const lines = buffer.split('\n');
    buffer = lines.pop(); // آخر سطر ناقص

    for (const line of lines) {
      if (!headers) {
        headers = line.split(',');
        continue;
      }
      const values = line.split(',');
      const row = Object.fromEntries(
        headers.map((h, i) => [h, values[i]])
      );
      this.push(JSON.stringify(row) + '\n');
    }
    cb();
  },
});

await pipeline(
  fs.createReadStream('data.csv'),
  csvToNdjson,
  fs.createWriteStream('data.ndjson')
);

التفصيل المهم: buffer = lines.pop() بيحتفظ بآخر سطر ناقص لحد ما الـ chunk اللي بعده يجي. من غير السطر ده، هتفقد سطرًا كل 64KB.

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

  • بتكسب: ذاكرة ثابتة بغض النظر عن حجم الملف. Latency أقل لأن أول byte بيخرج قبل ما كل الإدخال يتقرا. شغلك بيشتغل على ملفات أكبر من الـ RAM بكتير.
  • بتخسر: كود أعقد من سطر readFile. Debugging أصعب لأن الأخطاء بتحصل middle-of-pipeline ومش سهل تعرف فين. مش تقدر تشوف الداتا كلها مرة واحدة — لازم منطقك يتعامل مع chunk في كل لحظة.

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

في ثلاث حالات واضحة. الأولى: لو الملف أصغر من 50MB وعندك ذاكرة كافية، fs.readFile أسرع وأبسط. Streams فيها overhead ثابت لكل chunk (حوالي 15-20% زيادة في الوقت لو الملف صغير). التانية: لو محتاج access عشوائي على الملف (seek بـ offset محدد)، streams مش الخيار — استخدم fs.read مع offset مباشرة. التالتة: لو بتعالج JSON واحد كبير لازم تبنيه object كامل قبل ما تستخدمه (مش JSON Lines)، streams وحدها مش هتساعدك. تحتاج مكتبة زي JSONStream أو stream-json عشان تعمل parsing incremental.

قياس الأداء: إزاي تتأكد إن streams شغالة صح

جرّب السكربت بـ memory limit منخفض:

Bash

node --max-old-space-size=256 script.js

لو السكربت كمل على ملف أكبر من 256MB بدون ما يقع، streams شغالة فعلًا. لو وقع بـ JavaScript heap out of memory، معناه في مكان في الكود بيراكم البيانات — غالبًا Array.push على نتائج معالجة، أو JSON.parse بعد ما تجمّع الملف كامل. افتح Chrome DevTools على --inspect وخد heap snapshot وهتلاقي المصدر في دقيقتين.

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

افتح أكبر ملف logs في مشروعك دلوقتي وجرّب السكربت الأول بـ node --max-old-space-size=256. لو مشي، streams شغالة صح. لو وقع، راجع الكود وابحث عن push على array خارج الـ loop أو concat على buffer — ده المصدر في 90% من الحالات. جرّب دلوقتي قبل ما تكمل قراءة، الفرق هتحسّه في أول run.

مصادر

  • Node.js Docs — Stream API: nodejs.org/api/stream.html
  • Node.js Docs — stream/promises pipeline: nodejs.org/api/stream — pipeline
  • Node.js Learn — Backpressuring in Streams: nodejs.org/learn/modules/backpressuring-in-streams
  • Node.js Docs — readline Interface: nodejs.org/api/readline.html
  • V8 Blog — String length limits: v8.dev/blog/heap-size-limits

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

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

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