جارٍ تحميل وحدات 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.