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 は、コンパイルとインスタンス化を 1 回で行うことができます。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 で楽しもう