WebAssembly モジュールを効率的に読み込む

WebAssembly で作業するときは、多くの場合、モジュールをダウンロードし、コンパイルしてインスタンス化し、JavaScript でエクスポートしたものを使用します。この投稿では、最適な効率を実現するための推奨アプローチについて説明します。

WebAssembly を使用する場合、モジュールをダウンロードし、コンパイルしてインスタンス化し、JavaScript でエクスポートしたものを使用することがよくあります。この投稿ではまず、最適とは言えない一般的なコード スニペットを紹介します。次に、考えられる最適化をいくつか取り上げ、JavaScript から 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 では、4 KB を超えるバッファに対して WebAssembly.Module を無効にします。サイズ制限を回避するには、代わりに 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) で、Chrome でも同じ 4 KB のバッファサイズ制限が適用されます。一貫性とメインスレッドを解放するために、非同期の 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 を使用します。この変更により、await fetch(url) によって返された Response インスタンスを直接渡すことができるため、中間配列バッファを取り除くこともできます。

(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 インスタンスに解決される Promise も受け入れます。コード内の他の場所で response が必要ない場合は、結果を明示的に await せずに、fetch によって返された Promise を直接渡すことができます。

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

インスタンスが 1 つだけ必要な場合は、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 をお楽しみください。