DanLevy.net

أوقف تسرب الذاكرة باستخدام WeakMap

إصلاح الكود الضعيف باستخدام المراجع الضعيفة!

هل تعرف ذلك الشعور عندما تغير سطرًا واحدًا من الكود وتشاهد استخدام الذاكرة ينخفض بنسبة 50%؟ عشت تلك اللحظة وأنا أشاهد مراقب أداء أدوات المطورين في Chrome بينما كان تطبيق لوحة تحكم ينزف 100 ميغابايت كل ساعة، ثم أصبح يعمل نظيفًا طوال فترة بعد الظهر.

التغيير في سطر واحد: new Map() أصبح new WeakMap().

هذا كل شيء. نفس واجهة API، نفس نمط الاستخدام، سلوك مختلف تمامًا تحت الغطاء. لكن فهم سبب نجاح هذا يعني فهم شيء لا يفكر فيه معظم مطوري JavaScript أبدًا: ماذا يحدث عندما لا ينظر أحد إلى بياناتك بعد الآن.

عندما تصبح المراجع مراسي

تعامل الخريطة العادية (Map) في JavaScript مفاتيحها كبضائع ثمينة. بمجرد أن تضع شيئًا ما فيها، ستتمسك به الخريطة بقبضة حديدية. يرى جامع القمامة (Garbage Collector) هذه العلاقة ويفكر: “من الواضح أنهم لا يزالون بحاجة إلى هذا الكائن، الأفضل عدم لمسه.”

تصبح هذه الغريزة الوقائية مشكلة عندما تخزن بيانات وصفية عن أشياء مؤقتة. عُقد DOM التي تُزال. جلسات المستخدم التي تنتهي صلاحيتها. مثيلات المكونات التي تُفك. الخريطة لا تعلم أن هذه الكائنات لم تعد مفيدة. إنها تعلم فقط أن لديها مرجعًا، لذا فهي تُبقيها حية.

const cache = new Map();
function trackClick(element) {
cache.set(element, { clicks: 0 });
}
document.body.removeChild(element);
// العنصر اختفى من DOM، لكن cache لا تزال تُبقيه في الذاكرة

لا يستطيع جامع القمامة تنظيف element لأن cache لا تزال تشير إليه. يُسمى هذا “مرجعًا قويًا”، وفي تطبيقات الصفحة الواحدة طويلة الأمد، يصبح تسريبًا يؤدي في النهاية إلى تعطل المتصفح.

WeakMap تغير القواعد

تعمل WeakMap بشكل مختلف. إنها تعامل مفاتيحها كمواطنين مؤقتين وليس كمقيمين دائمين. عندما تخزن شيئًا في WeakMap، فأنت تقول بشكل أساسي: “أريد ربط هذه البيانات بهذا الكائن، لكنني لا أريد أن أكون السبب في بقائه حيًا.”

إذا كان الشيء الوحيد الذي يُبقي كائنًا ما في الذاكرة هو WeakMap، يُسمح لجامع القمامة بأخذه. عندما يختفي الكائن، يختفي معه إدخال WeakMap. لا حاجة للتنظيف اليدوي.

const cache = new WeakMap();
function trackClick(element) {
cache.set(element, { clicks: 0 });
}
document.body.removeChild(element);
// يتم جمع العنصر بواسطة جامع القمامة
// يختفي إدخال cache تلقائيًا

أجريت اختبار أداء بإنشاء 100,000 عقدة DOM، وتخزين بيانات وصفية لكل منها، ثم إزالتها جميعًا. باستخدام Map، احتفظ المتصفح بـ 150-200 ميغابايت. باستخدام WeakMap، انخفض إلى 70-80 ميغابايت. نفس الكود، نفس الوظيفة، نصف استهلاك الذاكرة.

ما الذي تتخلى عنه

لدى WeakMap قيود قد تبدو كحدود حتى تدرك أنها ما يجعل السحر يعمل.

لا يمكنك التكرار على WeakMap. لا forEach، ولا keys()، ولا values(). هذا منطقي عندما تفكر فيه: قد يحذف جامع القمامة إدخالًا في منتصف الحلقة. هل تريد حقًا التعامل مع ذلك؟

لا يمكنك التحقق من الحجم. لا خاصية .size، ولا .length. مرة أخرى، إنه هدف متحرك. قد يتغير الرقم بينما تسأل وعندما تحصل على الإجابة.

يجب أن تكون المفاتيح كائنات. لا سلاسل نصية، ولا أرقام، ولا قيم بدائية. هذا أساسي لكيفية عمل المراجع الضعيفة: القيم البدائية ليس لها هوية يمكن تتبعها بشكل منفصل عن قيمتها.

هذه ليست أخطاء. إنها التصميم. WeakMap مبني لمهمة واحدة محددة: إرفاق بيانات وصفية بالكائنات دون منع تنظيف تلك الكائنات. إذا كنت بحاجة إلى التكرار أو مفاتيح بدائية أو عدد الإدخالات، فأنت على الأرجح تحل مشكلة مختلفة ويجب عليك استخدام Map عادي.

أين يساعد هذا فعليًا

كان نمط “البيانات الخاصة” هو حالة الاستخدام الأصلية لـ WeakMap، قبل أن تحتوي JavaScript على حقول #private. كانت المكتبات تنشئ WeakMap خارج الفئة وتستخدمه لتخزين بيانات لا ينبغي أن تكون قابلة للوصول على المثيل.

const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}

عندما يتم جمع مثيل User بواسطة جامع القمامة، تذهب البيانات الخاصة معه. لا حاجة لكود تنظيف.

التخزين المؤقت للنتائج (Memoization) مناسب طبيعي آخر، خاصة عندما تخزّن النتائج بناءً على مدخلات كائنات بدلاً من القيم البدائية. إذا كانت حساباتك المكلفة تأخذ كائن تهيئة كمدخل، فإن WeakMap يعني أنك لا تقلق من بقاء الذاكرة المؤقتة أطول من الكائنات.

const cache = new WeakMap();
function expensiveCalc(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj);
cache.set(obj, result);
return result;
}

الذاكرة المؤقتة تعيش فقط طالما أن الكائنات المخزنة موجودة. بمجرد أن لا يُشار إلى obj في أي مكان آخر، تختفي النتيجة المخزنة وإدخال الذاكرة المؤقتة معًا.

متى تستخدمه

تسرب الذاكرة في تطبيقات الويب الحديثة يأتي عادة من المراجع القديمة لأشياء كان يجب تنظيفها. إذا كنت تبني أي شيء طويل الأمد، لوحة تحكم تبقى مفتوحة طوال اليوم، تطبيق دردشة يعمل لساعات، لوحة إدارة لا تُحدّث أبدًا، عليك التفكير في ما يحدث للبيانات القديمة.

WeakMap مفيد بشكل خاص عندما تربط بيانات بعقد DOM، أو مثيلات مكونات، أو أي كائن لا تتحكم في عمره. إذا كنت تخزن شيئًا بناءً على مرجع وقد يختفي هذا المرجع، فإن WeakMap يجعل التنظيف أسهل بكثير.

Map العادي لا يزال الخيار الصحيح عندما تبني ذاكرة مؤقتة فعلية بسياسات إزالة، عندما تحتاج إلى التكرار على الإدخالات، عندما تستخدم مفاتيح بدائية، أو عندما تكون البيانات نفسها هي المهمة وليس ارتباطها بكائن.

الشيء الجميل في WeakMap هو أنه عادة ما يكون واضحًا متى تحتاجه. إذا وجدت نفسك تكتب كود تنظيف لإزالة إدخالات الخريطة عندما تُدمر الكائنات، فهذه علامة. إذا كنت قلقًا من نمو الذاكرة بلا حدود لأنك لست متأكدًا متى تحذف الأشياء، فهذه علامة أخرى.

أحيانًا تكون أفضل ميزة هي التي تعمل فقط دون أن تضطر للتفكير فيها.

الموارد