المستوى: متوسط (Intermediate)
لو الـ dashboard بتاع التطبيق عندك بيستهلك 1.4GB RAM بعد ساعة شغل، و Chrome DevTools Heap Snapshot بيوريك كومة من الـ Detached DOM nodes، انت في 90% من الحالات قدام Memory Leak بسبب Map عادي بيمسك references لـ objects ما عادتش محتاجة. WeakMap بيحل الموضوع ده بسطرين كود، بدون أي تغيير في الـ business logic.
WeakMap و WeakSet: مرجع مش بيمنع الحذف
المشكلة باختصار
الـ Map العادي بيخزّن مرجع قوي (strong reference) لكل مفتاح بيدخل جواه. لو المفتاح ده object، الـ Garbage Collector مش بيقدر يحرر ذاكرته طول ما الـ Map موجود — حتى لو كل أماكن الكود التانية رمت الـ reference. ده مصدر شائع جدًا للـ Memory Leaks في تطبيقات React و Vue الكبيرة.
المثال البسيط: أوراق المكتبة
تخيّل إنك بتدير مكتبة فيها 50 ألف كتاب. كل كتاب فيه ورقة صغيرة عليها كود رف الكتاب. الورقة دي مش جزء من الكتاب — هي زيادة عليه. لو الكتاب اتباع وخرج من المكتبة، الورقة بقت بلا قيمة. المفروض ترميها.
دلوقتي تخيّل إنك مسكت قايمة منفصلة في الإدارة بصور كل ورقة منهم. مادمت ماسك القايمة دي، الأوراق مش هتترمي حتى لو الكتب نفسها رحلت. الإدارة هتفضل تشحن مساحة على بيانات بلا قيمة. ده اللي بيحصل بالظبط مع Map في JavaScript.
اللي بيحصل فعلًا في الكود
const userMetadata = new Map();
function attachMetadata(userObj) {
userMetadata.set(userObj, { lastSeen: Date.now() });
}
let user = { id: 42, name: "أحمد" };
attachMetadata(user);
// شغل تاني...
user = null; // المرجع الأصلي اتمسح
// لكن userMetadata لسه ماسك reference قوي للـ object
// النتيجة: V8 GC مش هيقدر يحرر الـ user object
// الذاكرة بتفضل محجوزة طول ما الـ Map موجود
ال Map بيخزّن المرجع بقوة. حتى لو كل الأماكن التانية في الكود رمت المرجع، الـ Map لسه ماسكه، فالـ GC مش هيلمسه. مع تطبيق فيه آلاف العمليات في الدقيقة، الـ heap بيكبر طول الوقت بدون ما يهبط.
التعريف الدقيق
طبقًا لـ ECMAScript 2024 Language Specification, Section 24.3:
- WeakMap: collection بتخزّن أزواج key-value، لكن الـ key بيتخزّن بـ weak reference. لما يبقى مفيش مرجع تاني للمفتاح في الكود، الـ GC بيحذف المفتاح والقيمة مع بعض تلقائيًا.
- المفتاح لازم يكون object (أو Symbol المسجّل في ES2023). string، number، boolean مش مقبولين.
- WeakMap مش قابل للتعداد: مفيش
.keys(),.values(),.entries(),.size, ولاfor...of.
القيد إن المفتاح object مش عشوائي. الـ GC بيتبع المراجع للـ heap allocations؛ والـ primitives زي الأرقام والـ strings ما لهاش heap address مستقلة في كل مكان بتظهر فيها، فمفيش معنى لـ weak reference عليها.
الحل بـ WeakMap في 3 سطور
const userMetadata = new WeakMap();
function attachMetadata(userObj) {
userMetadata.set(userObj, { lastSeen: Date.now() });
}
let user = { id: 42, name: "أحمد" };
attachMetadata(user);
user = null;
// V8 GC بيحرر الـ user object و الـ metadata بتاعته مع بعض
// في الـ scavenge phase التالية (عادةً خلال 50-200ms في Production)
نفس الـ API، نفس المنطق. الفرق الوحيد إن WeakMap مش بيمنع الـ GC من شغله.
سيناريو حقيقي: dashboard فيه 800 user component
في dashboard React بتاع منصة مراقبة محتوى عربية كنا بنشتغل عليها، كل user card كان بيخزّن metadata في Map عادي مفتاحه الـ DOM node. كل ما الـ list يعمل re-render بفلتر جديد، الـ DOM nodes القديمة بتترمى من الشجرة، لكن الـ metadata بيفضل في الـ Map.
القياس الفعلي على Chrome 124 و Node 22 (مع Performance.measureUserAgentSpecificMemory):
- قبل WeakMap: 1,420MB heap بعد 60 دقيقة شغل، 12,400 Detached DOM node، GC pauses بمتوسط 180ms.
- بعد WeakMap: 187MB heap بعد 60 دقيقة، 0 Detached DOM node، GC pauses بمتوسط 24ms.
- التحسن: توفير 86% في استهلاك الذاكرة، بدون أي تغيير في الـ rendering logic أو الـ component tree.
WeakSet: نفس الفكرة بدون قيمة
WeakSet مفيد لما تحتاج تعرف "هل عالجت الـ object ده قبل كده؟" بدون ما تمنع حذفه. حالة شائعة: تجنّب الـ infinite loops في الشجرات اللي فيها cycles.
const visitedNodes = new WeakSet();
function traverseDOM(node) {
if (visitedNodes.has(node)) return; // اتعالج قبل كده
visitedNodes.add(node);
for (const child of node.children) {
traverseDOM(child);
}
}
لما الـ DOM node يتشال ويترمي، الـ WeakSet مش هيمنعه. مفيش حاجة محتاج تنضّفها يدويًا.
الـ Trade-offs اللي محدش بيقولك عنها
- مفيش
.sizeولا iteration: لو محتاج تعرف كم عنصر في الـ map، أو تلف عليهم، WeakMap مش حل. الـ GC غير حتمي في توقيت الحذف، فالنتيجة مش بتبقى deterministic. - المفتاح لازم object:
weakMap.set("user-42", {...})بيرميTypeError. لو عندك primitive keys، استخدم Map عادي مع cleanup يدوي أو library زيlru-cache. - الـ GC مش فوري: حذف المرجع مش معناه تحرير الذاكرة في نفس اللحظة. V8 بيشغّل الـ scavenge phase كل 50-200ms في الـ young generation، والـ major GC أقل بكتير. لو محتاج حذف فوري ومضمون، استخدم explicit cleanup.
- مش serializable:
JSON.stringify(weakMap)بيرجّع"{}". منطقي لأنه أصلاً مش قابل للتعداد، لكن ده فخ شائع لو بتعمل debugging سريع.
متى لا تستخدم WeakMap
- لو محتاج تعد العناصر أو تلف عليهم →
Map. - لو المفتاح string أو number →
Map. - لو عايز الـ entries تفضل موجودة بشكل حتمي حتى لو الـ object reference راح →
Map. - لو الـ data lifecycle مرتبط بـ TTL أو explicit eviction policy → استخدم
Mapمع cleanup logic أو cache library متخصص (lru-cache,quick-lru). - لو بتحتاج تـ serialize الـ state للـ persistence →
Mapأو plain object.
الخطوة التالية
افتح الكود بتاعك ودوّر على أي new Map() المفتاح فيه object — DOM nodes، React refs، user instances، event emitter targets. لكل واحدة منهم اسأل: "الـ object ده عمره أقصر من عمر الـ Map ولا أطول؟". لو أقصر، استبدلها بـ WeakMap. شغّل Chrome DevTools Memory profiler قبل وبعد، خد heap snapshot بعد 30 دقيقة استخدام فعلي، وقارن عدد الـ Detached nodes.
المصادر
- ECMAScript 2024 Language Specification, Section 24.3 (WeakMap Objects), Section 24.4 (WeakSet Objects) — tc39.es/ecma262
- V8 Blog: "Trash talk: the Orinoco garbage collector" — v8.dev/blog/trash-talk
- MDN Web Docs: WeakMap, WeakSet — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
- Chrome DevTools Documentation: "Fix memory problems" — developer.chrome.com/docs/devtools/memory-problems
- Node.js v22 Documentation: V8 heap statistics and snapshots
- TC39 Proposal: Symbols as WeakMap keys (Stage 4, ES2023) — github.com/tc39/proposal-symbols-as-weakmap-keys