بارگیری موثر ماژول های WebAssembly

هنگام کار با WebAssembly، اغلب می خواهید یک ماژول را دانلود کنید، آن را کامپایل کنید، نمونه سازی کنید، و سپس از هر آنچه که در جاوا اسکریپت صادر می کند استفاده کنید. این پست روش پیشنهادی ما را برای بهره وری بهینه توضیح می دهد.

هنگام کار با WebAssembly، اغلب می خواهید یک ماژول را دانلود کنید، آن را کامپایل کنید، نمونه سازی کنید، و سپس از هر آنچه که در جاوا اسکریپت صادر می کند استفاده کنید. این پست با یک قطعه کد رایج اما غیربهینه شروع می شود که دقیقاً همین کار را انجام می دهد، چندین بهینه سازی ممکن را مورد بحث قرار می دهد و در نهایت ساده ترین و کارآمدترین راه اجرای WebAssembly از جاوا اسکریپت را نشان می دهد.

این قطعه کد رقص کامل دانلود، کامپایل و لحظه ای را انجام می دهد، البته به روشی کمتر از حد مطلوب:

از این استفاده نکنید!

(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) برای تبدیل بافر پاسخ به ماژول استفاده می کنیم. این یک API همزمان است، به این معنی که رشته اصلی را تا زمانی که کامل شود مسدود می کند. برای جلوگیری از استفاده از آن، 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.compile از WebAssembly.compileStreaming استفاده کنید. این تغییر همچنین به ما این امکان را می دهد که از شر بافر آرایه میانی خلاص شویم، زیرا اکنون می توانیم نمونه 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 API نیز قولی را می پذیرد که به یک نمونه 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 API این کار را به صورت جریانی انجام می دهد:

(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);
})();

بهینه سازی هایی که ما اعمال کردیم را می توان به صورت زیر خلاصه کرد:

  • از APIهای ناهمزمان برای جلوگیری از مسدود کردن رشته اصلی استفاده کنید
  • از API های جریان برای کامپایل و نمونه سازی سریعتر ماژول های WebAssembly استفاده کنید
  • کدی را که به آن نیاز ندارید ننویسید

با WebAssembly لذت ببرید!