هذا المقال يتطلب مستوى: محترف
لو خدمة Node.js بتاعتك بتاكل 11GB RAM علشان تقرأ ملف CSV حجمه 12GB، المشكلة مش في V8 ولا في حجم الـ heap. أنت بتحمّل الملف كله في Array قبل ما تشتغل على أول سطر. Async Iterators في 30 سطر بتنزّل الاستهلاك لـ 78MB ثابت، بدون ما تخسر صف واحد من البيانات.
Async Iterators: استهلك Streams ضخمة بدون انهيار الذاكرة
المشكلة باختصار
أي خدمة بتعمل ETL أو Data Migration بتمر بنفس الفخ: الـ developer بيقرأ ملف، بيـ split على \n، وبيعمل forEach. الكود شغّال على ملف 100MB. بيقع على ملف 12GB بـ Out Of Memory. Node.js مش بطيئة هنا. المشكلة إن الكود بيحجز ذاكرة بحجم الملف كله قبل ما يبدأ المعالجة، وفي الإنتاج ده بيتحوّل لـ container restarts وفقدان بيانات.
مثال للمبتدئ: ماسورة المياه vs الخزان الكبير
تخيّل عندك خزّان مياه 10 أمتار مكعب وعايز تنقّل المياه لخزان تاني. فيه طريقتين. الأولى: تفرّغ الخزان الأصلي كله في حوض ضخم، وبعدين تنقل من الحوض للخزان الجديد. الثانية: توصّل ماسورة مباشرة بين الخزانين، فالمياه بتتحرّك بالتدريج بدون ما تحتاج مكان وسيط بحجم 10 أمتار مكعب.
الحوض الكبير هو الـ Array. الماسورة هي الـ Async Iterator. الفرق إن الـ Array بياخد ذاكرة بحجم البيانات كلها مرة واحدة، أما الـ Iterator بياخد ذاكرة بحجم العنصر الواحد بس. ده الفرق اللي بيخلّي كود واحد يقع على ملف 12GB، وكود تاني يعالجه في 78MB.
التعريف العلمي الدقيق
Async Iterator هو object بيلتزم بـ protocol معرّف في ECMA-262 Section 27.1.4. الـ object لازم يحتوي على method اسمها Symbol.asyncIterator بترجع object فيه method next() بترجع Promise لـ {value, done}. الميزة الجوهرية إن JavaScript engine بيقدر يطلب القيمة التالية بس لمّا الكود الاستهلاكي يكون جاهز، فالذاكرة بتفضل ثابتة بغض النظر عن حجم المصدر. في Node.js، أغلب الـ Readable Streams بتدعم البروتوكول ده تلقائيًا منذ الإصدار 10، والـ for-await-of هو السكر النحوي اللي بيخفي تعقيد الـ Promise chain.
الكود التنفيذي على Node.js 22
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
async function* readCsvLines(filePath) {
const stream = createReadStream(filePath, { encoding: 'utf8' });
const lines = createInterface({ input: stream, crlfDelay: Infinity });
let header = null;
for await (const line of lines) {
if (!header) {
header = line.split(',');
continue;
}
const values = line.split(',');
yield Object.fromEntries(header.map((k, i) => [k, values[i]]));
}
}
async function migrate(filePath) {
let count = 0;
for await (const row of readCsvLines(filePath)) {
await insertIntoDb(row);
if (++count % 10000 === 0) console.log(`processed: ${count}`);
}
}