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

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

في الرسم البياني التالي، يمكنك ملاحظة حدوث الكثير من أحداث جمع المهملات، ما قد يضرّ بأداء تطبيقات الويب. ستتناول هذه المقالة كيفية التحكّم في استخدام الذاكرة، ما يقلل من التأثير في أدائك.
تكاليف جمع القمامة والأداء
يستند نموذج الذاكرة في JavaScript إلى تقنية تُعرف باسم جامع المهملات. في العديد من اللغات، يكون المبرمج مسؤولاً مباشرةً عن تخصيص الذاكرة وإخلاءها من ذاكرة الرموز البرمجية في النظام. ومع ذلك، يدير نظام جمع المهملات هذه المهمة نيابةً عن المبرمج، ما يعني أنّه لا يتم تحرير العناصر من الذاكرة مباشرةً عندما يزيل المبرمج مرجعها، بل في وقت لاحق عندما تقرّر الأساليب الاستكشافية لجمع المهملات أنّه من المفيد إجراء ذلك. تتطلّب عملية اتخاذ القرار هذه أن ينفّذ نظام جمع المهملات بعض التحليلات الإحصائية على العناصر النشطة وغير النشطة، ما يستغرق فترة زمنية لتنفيذه.
غالبًا ما يتم تصوير عملية جمع المهملات على أنّها نقيض إدارة الذاكرة اليدوية، والتي تتطلّب من المبرمج تحديد العناصر التي يجب إلغاء تخصيصها وإعادتها إلى نظام الذاكرة.
إنّ عملية استرداد الذاكرة من خلال "جمع القمامة" ليست مجانية، بل تؤثر عادةً في الأداء المتاح من خلال تخصيص فترة زمنية لتنفيذ عملها، بالإضافة إلى ذلك، يتّخذ النظام نفسه القرار بشأن وقت التشغيل. لا يمكنك التحكّم في هذا الإجراء، إذ يمكن أن يحدث نبضة GC في أي وقت أثناء تنفيذ الرمز البرمجي، ما سيؤدي إلى حظر تنفيذ الرمز البرمجي إلى أن يكتمل. لا يمكنك معرفة مدة هذا النبضة بشكل عام، وستستغرق بعض الوقت لتنفيذها، وذلك استنادًا إلى كيفية استخدام برنامجك للذاكرة في أي وقت معيّن.
تعتمد تطبيقات الأداء العالي على حدود أداء متّسقة لضمان تجربة سلسة للمستخدمين. يمكن أن تؤدي أنظمة جمع المهملات إلى تعطيل هذا الهدف، لأنّها يمكن أن تعمل في أوقات عشوائية لمدّات عشوائية، ما يستهلك الوقت المتاح الذي يحتاجه التطبيق لتحقيق أهداف الأداء.
تقليل معدّل تغيُّر الذاكرة، وتقليل الضرائب المفروضة على جمع المهملات
كما هو موضّح، سيحدث نبضة GC بعد أن تحدّد مجموعة من الاستدلالات أنّ هناك عددًا كافيًا من العناصر غير النشطة التي سيكون من المفيد إجراء نبضة لها. وبالتالي، فإنّ مفتاح تقليل الوقت الذي يستغرِقه "جامع المهملات" من تطبيقك يكمن في إزالة أكبر عدد ممكن من حالات إنشاء الكائنات وإزالتها بشكل مفرط. تُعرف عملية إنشاء/إخلاء العناصر بشكل متكرر باسم "التغييرات في الذاكرة". وإذا كان بإمكانك تقليل التغييرات في الذاكرة أثناء مدة تشغيل تطبيقك، يمكنك أيضًا تقليل الوقت الذي يستغرقه "جمع القمامة" من وقت التنفيذ. وهذا يعني أنّك بحاجة إلى إزالة / تقليل عدد العناصر التي تم إنشاؤها وإزالتها، وبالتالي عليك إيقاف تخصيص الذاكرة.
ستؤدي هذه العملية إلى نقل الرسم البياني للذكريات من :

إلى ما يلي:

في هذا النموذج، يمكنك ملاحظة أنّ الرسم البياني لم يعُد يتضمّن نمطًا يشبه سن المنشار، بل ينمو كثيرًا في البداية، ثم يزداد ببطء بمرور الوقت. إذا كنت تواجه مشاكل في الأداء بسبب سرعة استهلاك الذاكرة، هذا هو نوع الرسم البياني الذي تريد إنشاؤه.
الانتقال إلى JavaScript ذات الذاكرة الثابتة
JavaScript للذاكرة الثابتة هي تقنية تتضمن التخصيص المُسبَق، في بداية تطبيقك، لجميع الذاكرة التي ستحتاج إليها طوال فترة تشغيله، وإدارة هذه الذاكرة أثناء التنفيذ عندما لا تكون الكائنات مطلوبة بعد الآن. يمكننا تحقيق هذا الهدف من خلال اتّباع بضع خطوات بسيطة:
- تجهيز تطبيقك لتحديد الحد الأقصى لعدد عناصر الذاكرة النشطة المطلوبة (حسب النوع) لمجموعة من سيناريوهات الاستخدام
- أعِد تنفيذ الرمز البرمجي لتخصيص هذا الحد الأقصى مسبقًا، ثم استرِده أو أعِد إصداره يدويًا بدلاً من الانتقال إلى الذاكرة الرئيسية.
في الواقع، يتطلّب تحقيق الهدف الأول تنفيذ بعض الإجراءات الواردة في الهدف الثاني، لذا لنبدأ من هناك.
مجموعة العناصر
بعبارة بسيطة، تجميع العناصر هو عملية الاحتفاظ بمجموعة من العناصر غير المستخدَمة التي تتشارك نوعًا معيّنًا. عندما تحتاج إلى عنصر جديد لرمزك، يمكنك إعادة تدوير أحد العناصر غير المستخدَمة من المجموعة بدلاً من تخصيص عنصر جديد من مساحة الذاكرة للنظام. بعد انتهاء الرمز الخارجي من استخدام العنصر، يتم إرجاعه إلى الحزمة بدلاً من تحريره في الذاكرة الرئيسية. وبما أنّه لا يتم أبدًا إلغاء الإشارة إلى العنصر (المعروف أيضًا باسم حذفه) من الرمز البرمجي، لن يتم جمع المهملات. يؤدي استخدام مجموعات العناصر إلى إعادة التحكّم في الذاكرة إلى المبرمج، ما يقلل من تأثير أداة جمع المهملات في الأداء.
بما أنّ هناك مجموعة غير متجانسة من أنواع العناصر التي يحتفظ بها التطبيق، يتطلّب الاستخدام الصحيح لحِزم العناصر توفُّر حزمة واحدة لكل نوع يشهد معدّلًا مرتفعًا للتغيير أثناء وقت تشغيل تطبيقك.
var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};
//..... do some stuff with the object that we need to do
gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference
بالنسبة إلى الغالبية العظمى من التطبيقات، ستتوقف في النهاية عن الحاجة إلى تخصيص عناصر جديدة. خلال عمليات التشغيل المتعدّدة لتطبيقك، من المفترض أن تتمكّن من معرفة الحدّ الأقصى المسموح به، ويمكنك تخصيص هذا العدد من العناصر مسبقًا في بداية تطبيقك.
تخصيص العناصر مسبقًا
سيمنحك تنفيذ ميزة تجميع الكائنات في مشروعك الحد الأقصى النظري لعدد الكائنات المطلوبة أثناء وقت تشغيل تطبيقك. بعد تشغيل موقعك الإلكتروني من خلال سيناريوهات اختبار مختلفة، يمكنك التعرّف بشكل جيد على أنواع متطلبات الذاكرة التي ستحتاج إليها، ويمكنك فهرسة هذه البيانات في مكان ما وتحليلها لمعرفة الحدود القصوى لمتطلبات الذاكرة لتطبيقك.
بعد ذلك، في إصدار التطبيق المخصّص للنشر، يمكنك ضبط مرحلة الإعداد لملء جميع مجموعات العناصر مسبقًا بكمية محدّدة. سيؤدي هذا الإجراء إلى نقل جميع عمليات بدء تشغيل العناصر إلى مقدمة تطبيقك، وخفض عدد عمليات التخصيص التي تحدث ديناميكيًا أثناء تنفيذه.
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
يرتبط المبلغ الذي تختاره بشكل كبير بسلوك تطبيقك، وفي بعض الأحيان لا يكون الحد الأقصى النظري هو الخيار الأفضل. على سبيل المثال، قد يؤدي اختيار متوسط الحد الأقصى إلى تقليل مساحة الذاكرة المستخدَمة للمستخدمين العاديين.
لا يُعدّ هذا الحلّ حلاً سحريًا.
هناك تصنيف كامل للتطبيقات التي يمكن أن تكون أنماط زيادة الذاكرة الثابتة فيها مفيدة. ومع ذلك، يشير زميلي في فريق Chrome DevRel ريناتو مانجيني إلى بعض السلبيات.
الخاتمة
تُعدّ لغة JavaScript مثالية للويب لأنّها سريعة وممتعة وسهلة البدء. ويعود السبب الرئيسي في ذلك إلى سهولة استخدامها وقلة القيود المفروضة على البنية النحوية وقدرتها على حلّ مشاكل الذاكرة نيابةً عنك. يمكنك إنشاء الرموز البرمجية والسماح لها بتنفيذ المهام الصعبة. ومع ذلك، بالنسبة إلى تطبيقات الويب العالية الأداء، مثل ألعاب HTML5، يمكن أن يستهلك GC غالبًا عدد اللقطات في الثانية المطلوب بشكلٍ كبير، ما يقلّل من تجربة المستخدم النهائي. من خلال بعض عمليات القياس الدقيقة واستخدام مجموعات الكائنات، يمكنك تقليل هذا العبء على معدّل عرض اللقطات واستعادة هذا الوقت لتنفيذ المزيد من الإجراءات الرائعة.
رمز المصدر
هناك الكثير من عمليات تنفيذ مجموعات الكائنات على الويب، لذا لن أُملّكك بشرح عملية أخرى. بدلاً من ذلك، سأوجّهك إلى هذه المقالات التي تتضمّن كلّ منها تفاصيل تنفيذ محدّدة، وهو أمر مهمّ، لأنّ كل استخدام للتطبيق قد يتطلّب تنفيذًا معيّنًا.
- مجموعة كائنات Gamecore.js
- Beej’s Object Pools
- مجموعة عناصر بسيطة للغاية من Emehrkay
- مجموعة الكائنات التي يركز عليها ستيفن لامبرت في الألعاب
- إعداد objectPool في RenderEngine