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.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 で楽しもう