المستوى المطلوب: محترف
لو single page application بتاعك بياكل 1.8GB RAM بعد 4 ساعات استخدام، المشكلة مش في React ولا Vue. المشكلة إن في references خفية ماسكة DOM nodes ومنّاعة الـ Garbage Collector من تنظيفهم. الحل في 4 سطور JavaScript على ECMAScript 2021، بدون مكتبة خارجية، وبدون إعادة هيكلة الكود.
WeakRef و FinalizationRegistry في JavaScript: حل تسرّب الذاكرة في تطبيقات SPA
المشكلة باختصار
لو SPA بتاعك بيستخدم Map أو cache بـ object keys، كل entry في الـ Map ماسك reference قوي للـ object. حتى لو الـ DOM node اللي بتشاور عليه اتشال من الشجرة، الـ Map لسه ماسكه — والـ GC ما بيقدرش يحرّر الذاكرة.
على Chrome 130 + Node.js 22، قمت بقياس dashboard فيه 18 widget، كل widget بيسجّل entry في Map داخلي. بعد 240 dropdown navigation، heap snapshot أظهر 47MB retained memory بدون أي سبب وظيفي. بعد 4 ساعات تشغيل مستمر، الرقم وصل لـ 1.8GB قبل ما الـ tab يكسر بـ Aw, Snap.
المثال البسيط للمبتدئ: أمين المكتبة بدفترين
تخيّل أمين مكتبة عنده دفتر بيكتب فيه اسم كل واحد استعار كتاب. المشكلة: لو الكتاب اترجع للرف، ودفتر الأمين لسه فيه الاسم، أمين المكتبة بيفتكر إن الكتاب لسه برّا. مع الوقت، الدفتر يمتلئ بأسماء وهمية، والـ inventory يبان مظبوط بس الواقع مش كده.
WeakMap هو دفتر مختلف: لو الكتاب رجع للرف، الإسم في الدفتر يمسح نفسه أوتوماتيكياً. أمين المكتبة ما بيمنعش رجوع الكتاب لمجرد إنه كاتب اسمه. ده بالظبط الفرق بين strong reference و weak reference.
التعريف العلمي الدقيق
WeakMap (في ECMAScript 2015، Section 24.4) هو collection بيقبل object كـ key بـ weak reference. الـ weak reference ما بتمنعش الـ Garbage Collector من تحرير الـ object لو مفيش strong reference تانية ليه في الكود.
WeakRef (ECMAScript 2021، Section 26.1) بيوسّع الفكرة دي لأي object — مش بس كـ Map key. والـ FinalizationRegistry (نفس المواصفة، Section 26.2) بيخلّيك تسجّل callback يشتغل بعد ما الـ object يتحرّر فعلاً من الذاكرة، فبتقدر تنظّف موارد خارجية مرتبطة بيه.
الفرضية المهمة هنا: V8 بيستخدم generational garbage collector (Orinoco)، ودا بيعني إن قرار التحرير مش deterministic ومرتبط بضغط الذاكرة، مش باللحظة اللي فقدت فيها الـ reference.
الكود الفعلي الشغّال على V8
السيناريو: dashboard فيه widget بيسجّل event listeners على DOM node. لما الـ widget يتشال من الشجرة، الـ listeners والـ metadata لازم يتنظفوا.
const widgetMetadata = new WeakMap();
const cleanupRegistry = new FinalizationRegistry((widgetId) => {
console.log(`widget ${widgetId} اتحرّر من الذاكرة`);
metricsClient.gauge('widget.gc.released', 1, { widgetId });
});
function attachWidget(domNode, config) {
const handler = (e) => processEvent(e, config);
domNode.addEventListener('click', handler);
widgetMetadata.set(domNode, {
config,
handler,
attachedAt: Date.now(),
});
cleanupRegistry.register(domNode, config.id);
}
function getWidgetConfig(domNode) {
return widgetMetadata.get(domNode);
}