جارٍ تحميل وحدات WebAssembly بكفاءة

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

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

يؤدي مقتطف الرمز هذا إلى تنفيذ عملية التنزيل بشكل فوري، وإن كان ذلك بطريقة دون المستوى الأمثل:

لا تستخدم هذه المعلومات.

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = new WebAssembly.Module(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

لاحظ كيف نستخدم new WebAssembly.Module(buffer) لتحويل المخزن المؤقت للاستجابة إلى وحدة. هذا هو واجهة برمجة التطبيقات المتزامنة، أي حظر سلسلة التعليمات الرئيسية حتى تكتمل. لمنع استخدامه، استخدم Chrome يؤدي هذا الخيار إلى إيقاف WebAssembly.Module في المستودعات التي يزيد حجمها عن 4 كيلوبايت. للتحايل على الحد الأقصى للحجم، يمكننا استخدِم await WebAssembly.compile(buffer) بدلاً من ذلك:

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

لا يزال await WebAssembly.compile(buffer) لا يزال هو النهج الأمثل، ولكننا سنصل إليه ثانيًا.

أصبحت كل عملية في المقتطف المعدَّل تقريبًا غير متزامنة الآن، حيث يؤدي استخدام await إلى وضوحًا. الاستثناء الوحيد هو new WebAssembly.Instance(module)، الذي يتضمّن المخزن المؤقت نفسه الذي يبلغ 4 كيلوبايت قيود الحجم في Chrome. للحفاظ على الاتساق وبغرض الاحتفاظ بسلسلة المحادثات الرئيسية مجانية، يمكننا استخدام البيانات غير المتزامنة WebAssembly.instantiate(module)

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

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

عندما يكون وقت التنزيل
أطول من وقت التجميع في وحدة WebAssembly، ثم WebAssembly.compileStreaming()
ينتهي التجميع فور تنزيل آخر وحدات البايت.

لتفعيل هذا التحسين، استخدِم WebAssembly.compileStreaming بدلاً من WebAssembly.compile. كما يتيح لنا هذا التغيير التخلص من المخزن المؤقت للصفيف المتوسط، حيث يمكننا الآن تمرير تم عرض Response نسخة افتراضية من قِبل await fetch(url) مباشرةً.

(async () => {
  const response = await fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(response);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

تقبل واجهة برمجة التطبيقات WebAssembly.compileStreaming أيضًا الوعد الذي يؤدي إلى Response مثال. إذا لم تكن بحاجة إلى استخدام response في أي مكان آخر في الرمز، يمكنك تنفيذ وعدك. التي يتم إرجاعها بواسطة fetch مباشرة، بدون awaitالنتيجة بشكل صريح:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(fetchPromise);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

وإذا لم تكن بحاجة إلى نتيجة fetch في مكان آخر، يمكنك تمريرها مباشرةً باتّباع الخطوات التالية:

(async () => {
  const module = await WebAssembly.compileStreaming(
    fetch('fibonacci.wasm'));
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

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

هل ترى كيفية تجميع الرد في وحدة ثم إنشاء مثيله على الفور؟ كما اتضح، بإمكان WebAssembly.instantiate التجميع وإنشاء مثيل في آنٍ واحد. تشير رسالة الأشكال البيانية تُجري واجهة برمجة التطبيقات WebAssembly.instantiateStreaming ذلك من خلال بث المحتوى:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  // To create a new instance later:
  const otherInstance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

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

// This is our recommended way of loading WebAssembly.
(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

ويمكن تلخيص التحسينات التي طبّقناها على النحو التالي:

  • استخدام واجهات برمجة التطبيقات غير المتزامنة لتجنُّب حظر سلسلة التعليمات الرئيسية
  • استخدِم واجهات برمجة التطبيقات للبث لتجميع وحدات WebAssembly وإنشاء مثيل لها بسرعة أكبر
  • عدم كتابة رمز لا تحتاجه

استمتع باستخدام WebAssembly