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 — بتمر على الداتا مرة واحدة من الأول للآخر.
الأربع أنواع اللي هتقابلك فعلاً
الـ 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 مش اختيار — الكود تحت هو الصح:
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 قديم: