مستوى المقال: متوسط
لو الـ Node.js process بتاعك بياكل RAM كل ساعة لحد ما يموت بـ JavaScript heap out of memory، الكود مش بيتسرّب بسرعة — هو بيحتفظ بمرجع لكائن مفروض ينتهي. Heap snapshots بتكشف الـ retainer في 3 خطوات بدون ما تضيف مكتبة، وهتقدر تنزّل الاستهلاك من 1.4GB لـ 180MB في process إنتاج فعلي.
Memory Leaks في Node.js: الدليل العملي للاكتشاف والإصلاح
المشكلة باختصار
عندك خدمة API بتاعك Node.js شغّالة على PM2، بتبدأ اليوم بـ 180MB، وبعد 6 ساعات بتوصل لـ 1.4GB، وبعدها بـ FATAL ERROR: Reached heap limit. الـ restart بيخفي المشكلة بس مش بيحلها. الـ Garbage Collector مش معطّل — في كود بيمسك references لكائنات مش محتاجها، فالـ GC ما يقدرش يحرّرها.
المقال ده هيوريك الـ workflow اللي بتشغّله مرة كل 3 شهور على أي خدمة Node.js: تاخد 2 heap snapshots بفارق زمني، تقارنهم، وتشوف بالظبط أي كائن بيكبر وميتشالش.
مثال للمبتدئ: صاحب البيت اللي مش بيرمي ورق
تخيّل شقة فيها صاحب بيت، كل يوم بيدخله بريد. الطبيعي إنه يقرا الجواب ويرميه. لكن لو هو بيحط كل جواب في درج "هرميه بعدين"، الدرج هيتملى، وبعد سنة الشقة كلها أدراج. مفيش حد بيقرا الجواب، ومفيش حد بيرميه. ده memory leak.
الـ V8 Garbage Collector زيّ عامل النظافة: بيدخل كل فترة، ويرمي اللي مفيش حد ماسكه. لكن لو في كود مدّاه قائمة فيها كل الجوابات (مثلاً Array.push في كل request) وما حدّش بيشيلها من القائمة، الـ GC هيقول "ده ليه مرجع، يبقى مهم"، وهيسيبه.
الشرح العلمي للـ V8 Heap
الـ V8 (المحرك اللي بيشغّل Node.js) بيقسّم الذاكرة لـ generations: New Space للكائنات الصغيرة الجديدة، وOld Space للكائنات اللي عاشت أكثر من 2 GC cycles. الـ GC بيشتغل بسرعة على New Space (Scavenge كل ~100ms) وبيشتغل ببطء على Old Space (Mark-Sweep-Compact كل ثواني).
الـ leak تقريبًا دايمًا في Old Space. لأنه كائن لما يعدّي 2 GC cycles ويلسّه مَرْجوع، بيترقّى للـ Old Space ويفضل هناك. لو في كود بيخلق كائنات Old كل request وما يفرّجش عنها، Old Space بيكبر خطيًا مع الوقت لحد ما يوصل --max-old-space-size (الافتراضي 1.4GB في Node 20 على 64-bit).
المرجع المسبّب للـ leak اسمه retainer. هدفك من heap snapshot هو إنك تشوف الـ retainer chain: مين بيمسك مين، لحد ما توصل للجذر (GC Root).