تصحيح أخطاء تسرب الذاكرة في WebAssembly باستخدام Emscripten

على الرغم من أنّ لغة JavaScript تتغاضى إلى حدّ كبير عن أهمية إزالة البرامج غير المرغوب فيها، فإنّ اللغات الثابتة ليست بالطبع...

Squoosh.app هو تطبيق ويب تقدّمي (PWA) يوضِّح مقدار برامج ترميز الصور المختلفة الإعدادات إلى تحسين حجم ملف الصورة بدون التأثير بشكل ملحوظ في الجودة. ومع ذلك، من عرض توضيحي تقني يعرض كيفية أخذ المكتبات المكتوبة بلغة C++ أو Rust إلى الويب.

تعتبر القدرة على نقل الرموز البرمجية من الأنظمة البيئية الحالية ذات قيمة كبيرة، ولكن هناك بعض الاختلافات بين تلك اللغات الثابتة وJavaScript. واحدة منها هي في نطاق مناهج لإدارة الذاكرة.

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

نمط مريب

في الآونة الأخيرة، أثناء بدء العمل على تطبيق Squoosh، لم أستطع المساعدة ولكن لاحظت نمطًا مثيرًا للاهتمام في برامج تضمين برنامج ترميز C++ لنلقِ نظرة على برنامج تضمين ImageQuant مثال (يتم تقليله لعرض أجزاء إنشاء العناصر وقطع البيع فقط):

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);

  // …

  free(image8bit);
  liq_result_destroy(res);
  liq_image_destroy(image);
  liq_attr_destroy(attr);

  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
}

void free_result() {
  free(result);
}

JavaScript (حسنًا، TypeScript):

export async function process(data: ImageData, opts: QuantizeOptions) {
  if (!emscriptenModule) {
    emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
  }
  const module = await emscriptenModule;

  const result = module.quantize(/* … */);

  module.free_result();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

هل لاحظت أي مشكلة؟ تلميح: انه use-after-free، ولكن JavaScript!

في Emscripten، يعرض typed_memory_view Uint8Array JavaScript المدعوم من WebAssembly (Wasm) مخزن مؤقت للذاكرة، مع ضبط byteOffset وbyteLength على المؤشر والطول المحدّدين. المصدر الرئيسي هي أن هذه طريقة عرض TypedArray في مخزن تخزين مؤقت للذاكرة WebAssembly، بدلاً من نسخة من البيانات تملكها JavaScript.

عندما نستدعي free_result من JavaScript، فإنّه يستدعي بدوره دالة C عادية free لوضع علامة هذه الذكرى على أنّها متاحة لأي عمليات تخصيص مستقبلية، ما يعني أنّ البيانات التي نعرضها على "Uint8Array" التي تشير إليها ببيانات عشوائية من خلال أي طلب مستقبلي إلى Wasm.

في الحالات الأخرى، قد يؤدّي تنفيذ free إلى عدم ملء الذاكرة الخالية على الفور. تشير رسالة الأشكال البيانية free التي يستخدمها Emscripten لا يفعل ذلك، ولكننا نعتمد على تفاصيل التنفيذ هنا لا يمكن ضمانها.

أو، حتى إذا تم الاحتفاظ بالذاكرة وراء المؤشر، قد تحتاج عملية تخصيص جديدة إلى زيادة ذاكرة WebAssembly عند زيادة WebAssembly.Memory إما من خلال JavaScript API أو يؤدّي تعليمات memory.grow إلى إلغاء ArrayBuffer الحالي، وبالتالي أي مشاهدات مدعومة به.

لنستخدم وحدة تحكّم أدوات مطوّري البرامج (أو Node.js) لتوضيح هذا السلوك:

> memory = new WebAssembly.Memory({ initial: 1 })
Memory {}

> view = new Uint8Array(memory.buffer, 42, 10)
Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ^ all good, we got a 10 bytes long view at address 42

> view.buffer
ArrayBuffer(65536) {}
// ^ its buffer is the same as the one used for WebAssembly memory
//   (the size of the buffer is 1 WebAssembly "page" == 64KB)

> memory.grow(1)
1
// ^ let's say we grow Wasm memory by +1 page to fit some new data

> view
Uint8Array []
// ^ our original view is no longer valid and looks empty!

> view.buffer
ArrayBuffer(0) {}
// ^ its buffer got invalidated as well and turned into an empty one

أخيرًا، حتى إذا لم نستدعِ Wasm مجددًا بين free_result وnew Uint8ClampedArray، قد نضيف في بعض الأحيان دعم سلاسل المحادثات المتعددة إلى برامج الترميز. في هذه الحالة، قد يكون مؤشر ترابط مختلف تمامًا يستبدل البيانات قبل أن ننجح في استنساخها.

البحث عن أخطاء الذاكرة

على سبيل الاحتياط، قررت المضي قدمًا والتحقق مما إذا كان هذا الرمز يعرض أي مشكلات عمليًا. هذه فرصة مثالية لتجربة المعقّمات الإلكترونية الجديدة الدعم الذي تمت إضافته العام الماضي وقد تم عرضها في محاضرة WebAssembly في مؤتمر Chrome Dev Summit:

في هذه الحالة، نهتم AddressSanitizer، التي اكتشاف العديد من المشكلات المتعلقة بالمؤشر والذاكرة. لاستخدامه، يجب إعادة تجميع برنامج الترميز مع -fsanitize=address:

emcc \
  --bind \
  ${OPTIMIZE} \
  --closure 1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s 'EXPORT_NAME="imagequant"' \
  -I node_modules/libimagequant \
  -o ./imagequant.js \
  --std=c++11 \
  imagequant.cpp \
  -fsanitize=address \
  node_modules/libimagequant/libimagequant.a

سيؤدي هذا الإجراء تلقائيًا إلى تفعيل عمليات التأكّد من أمان المؤشر، ولكنّنا نريد أيضًا العثور على الذاكرة المحتمَلة التسريبات. بما أننا نستخدم ImageQuant كمكتبة بدلاً من برنامج، فلا توجد "نقطة خروج" بنسبة الذي يستطيع Emscripten التحقق تلقائيًا من أنه تم إخلاء كل الذاكرة.

بدلاً من ذلك، في مثل هذه الحالات، يوفر LeakSanitizer (المضمّن في AddressSanitizer) الدوال __lsan_do_leak_check و __lsan_do_recoverable_leak_check, والذي يمكن استدعاءه يدويًا متى كنا نتوقع أن تكون كل الذاكرة فارغة ونرغب في التحقق من ذلك وافتراض العمل. الغرض من استخدام __lsan_do_leak_check في نهاية تطبيق قيد التشغيل، عندما تريد إلغاء العملية في حال اكتشاف أي تسرُّب، في حين أنّ "__lsan_do_recoverable_leak_check" هي أكثر ملاءمة لحالات استخدام المكتبة مثل حالتنا، عندما تريد طباعة تسريبات إلى وحدة التحكم، ولكن الحفاظ على تشغيل التطبيق بغض النظر عن ذلك.

سنعرض المساعد الثاني هذا عبر Embind كي نتمكّن من استدعائه من JavaScript في أي وقت:

#include <sanitizer/lsan_interface.h>

// …

void free_result() {
  free(result);
}

EMSCRIPTEN_BINDINGS(my_module) {
  function("zx_quantize", &zx_quantize);
  function("version", &version);
  function("free_result", &free_result);
  function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}

واستدعِها من جانب JavaScript بمجرد الانتهاء من الصورة. يعد القيام بذلك من ويساعد جانب JavaScript، بدلاً من لغة C++، في ضمان مشاركة جميع النطاقات وتم إخلاء جميع كائنات C++ المؤقتة عند إجراء عمليات الفحص هذه:

  // 

  const result = opts.zx
    ? module.zx_quantize(data.data, data.width, data.height, opts.dither)
    : module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);

  module.free_result();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

يُعطينا هذا تقريرًا مثل ما يلي في وحدة التحكم:

لقطة شاشة لرسالة

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

emcc \
  --bind \
  ${OPTIMIZE} \
  --closure 1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s 'EXPORT_NAME="imagequant"' \
  -I node_modules/libimagequant \
  -o ./imagequant.js \
  --std=c++11 \
  imagequant.cpp \
  -fsanitize=address \
  -g2 \
  node_modules/libimagequant/libimagequant.a

يبدو هذا أفضل بكثير:

لقطة شاشة لرسالة بعنوان &quot;التسريب المباشر بسعة 12 بايت&quot; قادم من الدالة GenericBindingType RawImage ::toWireType

لا تزال بعض أجزاء تتبع التكديس تبدو غامضة لأنها تشير إلى عناصر Emscripten الداخلية، ولكن يمكننا معرفة أنّ التسرّب يأتي من إحالة ناجحة "RawImage" إلى "نوع سلك" (إلى قيمة JavaScript) من خلال ربط. في الواقع، عندما ننظر إلى الرمز، يتضح لنا أننا نرجع RawImage مثيل من C++ إلى جافا سكريبت، ولكننا لا نحررها مطلقًا من أي جانب.

وللتذكير، لا يوجد في الوقت الحالي تكامل لمجموعة البيانات غير المهمة بين جافا سكريبت WebAssembly، على الرغم من أنّ أحدها قيد التطوير. بدلا من ذلك، لإخلاء أي ذاكرة وعوامل إلغاء الاستدعاء يدويًا من جانب JavaScript بمجرد الانتهاء من الخاص بك. بالنسبة إلى Embind على وجه التحديد، يمثِّل الرسم البياني المستندات اقتراح استدعاء طريقة .delete() في فئات C++ المكشوفة:

يجب أن يحذف رمز JavaScript الصريح أي معالجات كائن C++ قد تلقّاها، أو رمز Emscripten لأجزاء من الذاكرة إلى أجل غير مسمى

var x = new Module.MyClass;
x.method();
x.delete();

في الواقع، عندما نطبق ذلك باستخدام JavaScript للصف:

  // 

  const result = opts.zx
    ? module.zx_quantize(data.data, data.width, data.height, opts.dither)
    : module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);

  module.free_result();
  result.delete();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

سيختفي التسرّب على النحو المتوقَّع.

رصد المزيد من المشاكل المتعلّقة بالمعقّمات

ويؤدي إنشاء برامج ترميز أخرى لتطبيق Squoosh باستخدام المعقّمات إلى الكشف عن مشكلات متشابهة وكذلك بعض المشكلات الجديدة. بالنسبة على سبيل المثال، ظهر لي هذا الخطأ في عمليات ربط MozJPEG:

لقطة شاشة لرسالة

لا يشكّل ذلك تسربًا، لكنّنا نكتب إلى ذكرى خارج الحدود المخصّصة لها ...[

بالتعمق في رمز MozJPEG، نجد أن المشكلة هنا تكمن في jpeg_mem_dest— نستخدمها لتخصيص وجهة ذاكرة لـ JPEG — يعيد استخدام القيم الحالية outbuffer وoutsize عندما يكونون غير صفرية:

if (*outbuffer == NULL || *outsize == 0) {
  /* Allocate initial buffer */
  dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
  if (dest->newbuffer == NULL)
    ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
  *outsize = OUTPUT_BUF_SIZE;
}

ومع ذلك، قمنا باستدعاءها دون تهيئة أي من هذين المتغيرين، مما يعني أن MozJPEG يكتب إلى عنوان ذاكرة عشوائي يحتمل أن يكون مخزنًا في تلك المتغيرات وقت المكالمة!

uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);

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

مشاكل متعلقة بالحالة المشتركة

أم أنّنا؟

نحن نعلم أن عمليات ربط برنامج الترميز لدينا تخزن بعضًا من الحالة بالإضافة إلى ينتج عنها بيانات ثابتة عامة والمتغيرات، ويحتوي MozJPEG على بعض الهياكل المعقدة بشكل خاص.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
  // …
}

ماذا لو تم إعداد بعض هذه الألعاب بشكل كسول في أول مرة ثم إعادة استخدامها بشكل غير صحيح في المستقبل يركض؟ بعد ذلك، لن تؤدي مكالمة واحدة مع معقّم إلى الإبلاغ عن المشكلة باعتبارها مشكلة.

لنحاول معالجة الصورة عدة مرّات من خلال النقر عشوائيًا على مستويات جودة مختلفة. في واجهة المستخدم. وفي الواقع، حصلنا الآن على التقرير التالي:

لقطة شاشة لرسالة

262,144 بايت، يبدو أنّه تم تسريب نموذج الصورة بالكامل من jpeg_finish_compress.

بعد مراجعة المستندات والأمثلة الرسمية، توصّلنا إلى أنّ jpeg_finish_compress لا يؤدي ذلك إلى تحرير الذاكرة التي خصصتها عبر طلب jpeg_mem_dest السابق، بل يؤدي فقط إلى تحرير بنية الضغط، فعلى الرغم من أن بنية الضغط هذه تعرف مسبقًا الوجهة... تنهيدة.

يمكننا حلّ هذه المشكلة من خلال إخلاء البيانات يدويًا في الدالة free_result:

void free_result() {
  /* This is an important step since it will release a good deal of memory. */
  free(last_result);
  jpeg_destroy_compress(&cinfo);
}

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

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

إنشاء برنامج تضمين آمن

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

أولاً، سنصلح مشكلة "استخدام بعد الاستخدام المجاني" من بداية المشاركة. لهذا، نحتاج إلى استنساخ البيانات من طريقة العرض المدعومة من WebAssembly قبل وضع علامة عليها على أنها مجانية من جانب JavaScript:

  // 

  const result = /*  */;

  const imgData = new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );

  module.free_result();
  result.delete();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
  return imgData;
}

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

وللقيام بذلك، نعيد هيكلة برنامج تضمين C++ للتأكد من أن كل استدعاء للدالة يدير عمله الخاص البيانات باستخدام المتغيرات المحلية. ثم يمكننا تغيير توقيع دالة free_result إلى قبول المؤشر مرة أخرى:

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_attr* attr = liq_attr_create();
  liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_result* res = nullptr;
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);
  uint8_t* result = (uint8_t*)malloc(size * 4);

  // 
}

void free_result() {
void free_result(uint8_t *result) {
  free(result);
}

ولكن بما أننا نستخدم Embind في Emscripten للتفاعل مع JavaScript، فإنه يمكننا أيضًا جعل واجهة برمجة التطبيقات أكثر أمانًا عن طريق إخفاء تفاصيل إدارة ذاكرة C++ تمامًا

لذلك، لننقل الجزء new Uint8ClampedArray(…) من JavaScript إلى جانب C++ باستخدام ربط. وبعد ذلك، يمكننا استخدامه لانساخ البيانات في ذاكرة JavaScript حتى قبل الرجوع من الدالة:

class RawImage {
 public:
  val buffer;
  int width;
  int height;

  RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");

RawImage quantize(/*  */) {
val quantize(/*  */) {
  // 
  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
  val js_result = Uint8ClampedArray.new_(typed_memory_view(
    image_width * image_height * 4,
    result
  ));
  free(result);
  return js_result;
}

لاحظ كيف يمكننا، بإجراء تغيير واحد، أن نضمن أن مصفوفة البايت الناتجة مملوكة لـ JavaScript ولم يتم الاحتفاظ بها من خلال ذاكرة WebAssembly، و التخلص من برنامج تضمين RawImage الذي تم تسريبه سابقًا أيضًا.

لم تعد لغة JavaScript بحاجة إلى القلق بشأن تحرير البيانات، ويمكنها استخدام النتائج أي كائن آخر تم جمعه من البيانات غير الصالحة:

  // 

  const result = /*  */;

  const imgData = new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );

  module.free_result();
  result.delete();
  // module.doLeakCheck();

  return imgData;
  return new ImageData(result, result.width, result.height);
}

يعني ذلك أيضًا أنّنا لم نعُد بحاجة إلى ربط free_result مخصّص من جهة C++:

void free_result(uint8_t* result) {
  free(result);
}

EMSCRIPTEN_BINDINGS(my_module) {
  class_<RawImage>("RawImage")
      .property("buffer", &RawImage::buffer)
      .property("width", &RawImage::width)
      .property("height", &RawImage::height);

  function("quantize", &quantize);
  function("zx_quantize", &zx_quantize);
  function("version", &version);
  function("free_result", &free_result, allow_raw_pointers());
}

وبوجه عام، أصبح رمز برنامج تضمين أكثر نظافة وأكثر أمانًا في الوقت نفسه.

وبعد ذلك، أجريتُ بعض التحسينات الطفيفة على رمز برنامج تضمين ImageQuant وتم تكرار إصلاحات إدارة الذاكرة المماثلة لبرامج الترميز الأخرى. إذا كنت مهتمًا بمعرفة المزيد من التفاصيل يمكنك الاطلاع على العلاقات العامة الناتجة هنا: إصلاحات الذاكرة لـ C++ وبرامج الترميز.

الخلاصات

ما الدروس التي يمكن أن نتعلمها ونشاركها من إعادة البناء هذه والتي يمكن تطبيقها على قواعد رموز أخرى؟

  • لا تستخدم طرق عرض الذاكرة التي تدعمها WebAssembly، بغض النظر عن اللغة التي تم إنشاؤها منها، بخلاف واستدعاء واحد. لا يمكنك الاعتماد على بقائهم على قيد الحياة لفترة أطول من ذلك، ولن تكون قادرًا لاكتشاف هذه الأخطاء بالوسائل التقليدية، لذا إذا احتجت إلى تخزين البيانات لوقت لاحق، فقم بنسخها إلى جانب JavaScript وتخزينها هناك.
  • استخدِم لغة لإدارة الذاكرة الآمنة أو برامج تضمين من النوع الآمن على الأقل بدلاً من يعمل على المؤشرات الأولية بشكل مباشر. لن يُنقذك هذا من الأخطاء على JavaScript 🔐 WebAssembly لكنّه سيقلل من ظهور الأخطاء القائمة بذاتها من خلال رمز اللغة الثابت.
  • بغض النظر عن اللغة التي تستخدمها، يمكنك تشغيل الرموز البرمجية مع المعقّمات أثناء تطويرها، إذ يمكنها مساعدتك في لن نكتشف المشكلات في رمز اللغة الثابت فحسب، بل سنلاحظ أيضًا بعض المشكلات عبر جافا سكريبت ︎ حدود WebAssembly، مثل نسيان طلب .delete() أو المرور بمؤشرات غير صالحة من جانب JavaScript.
  • إذا أمكن، تجنَّب عرض البيانات والعناصر غير المُدارة من WebAssembly إلى JavaScript تمامًا. تُعد JavaScript لغة تجمع البيانات غير المرغوب فيها، ولا تستخدم الإدارة اليدوية للذاكرة فيها. يمكن اعتبار ذلك أحد التسرُّب التجريدي لنموذج الذاكرة للّغة WebAssembly ويمكن تجاهل الإدارة غير الصحيحة في قاعدة رموز JavaScript.
  • قد يكون هذا واضحًا، ولكن، كما هو الحال في أي قاعدة رموز برمجية أخرى، تجنَّب تخزين حالة قابلة للتغيير في القائمة المتغيرات. احرص على عدم تصحيح الأخطاء المتعلقة بإعادة استخدامه عبر الاستدعاءات المختلفة أو حتى الرسائل، لذا من الأفضل أن تكون مستقلة قدر الإمكان.