Memuat modul WebAssembly secara efisien

Saat menggunakan WebAssembly, Anda sering kali ingin mendownload modul, mengompilasi, membuat instance, lalu menggunakan apa pun yang diekspornya dalam JavaScript. Postingan ini menjelaskan pendekatan yang direkomendasikan untuk efisiensi yang optimal.

Saat menggunakan WebAssembly, Anda sering kali ingin mendownload modul, mengompilasi, membuat instance, lalu menggunakan apa pun yang diekspornya dalam JavaScript. Postingan ini dimulai dengan cuplikan kode umum, tetapi tidak optimal, yang melakukan hal tersebut, membahas beberapa kemungkinan pengoptimalan, dan pada akhirnya menunjukkan cara paling sederhana dan paling efisien untuk menjalankan WebAssembly dari JavaScript.

Cuplikan kode ini melakukan download-kompilasi-pembuatan instance lengkap, meskipun dengan cara yang kurang optimal:

Jangan gunakan ini!

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

Perhatikan cara kami menggunakan new WebAssembly.Module(buffer) untuk mengubah buffering respons menjadi modul. Ini adalah API sinkron, yang berarti API ini memblokir thread utama hingga selesai. Untuk mencegah penggunaannya, Chrome menonaktifkan WebAssembly.Module untuk buffer yang lebih besar dari 4 KB. Untuk mengatasi batas ukuran, kita dapat menggunakan 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) masih bukan pendekatan yang optimal, tetapi kita akan membahasnya dalam beberapa detik.

Hampir setiap operasi dalam cuplikan yang diubah kini bersifat asinkron, seperti yang dijelaskan oleh penggunaan await. Satu-satunya pengecualian adalah new WebAssembly.Instance(module), yang memiliki batasan ukuran buffer 4 KB yang sama di Chrome. Untuk konsistensi dan agar thread utama tetap bebas, kita dapat menggunakan WebAssembly.instantiate(module) asinkron.

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

Mari kita kembali ke pengoptimalan compile yang saya singgung sebelumnya. Dengan kompilasi streaming, browser sudah dapat mulai mengompilasi modul WebAssembly saat byte modul masih didownload. Karena download dan kompilasi terjadi secara paralel, proses ini lebih cepat — terutama untuk payload besar.

Jika waktu download
lebih lama dari waktu kompilasi modul WebAssembly, WebAssembly.compileStreaming()
akan menyelesaikan kompilasi hampir segera setelah byte terakhir didownload.

Untuk mengaktifkan pengoptimalan ini, gunakan WebAssembly.compileStreaming, bukan WebAssembly.compile. Perubahan ini juga memungkinkan kita menghapus buffering array perantara, karena sekarang kita dapat meneruskan instance Response yang ditampilkan oleh await fetch(url) secara langsung.

(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 juga menerima promise yang di-resolve ke instance Response. Jika Anda tidak memerlukan response di tempat lain dalam kode, Anda dapat meneruskan promise yang ditampilkan oleh fetch secara langsung, tanpa await hasilnya secara eksplisit:

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

Jika tidak memerlukan hasil fetch di tempat lain, Anda bahkan dapat meneruskannya secara langsung:

(async () => {
  const module = await WebAssembly.compileStreaming(
    fetch('fibonacci.wasm'));
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Namun, saya pribadi merasa lebih mudah membaca teks ini jika ditulis dalam baris terpisah.

Lihat cara kita mengompilasi respons ke dalam modul, lalu langsung membuat instance-nya? Ternyata, WebAssembly.instantiate dapat mengompilasi dan membuat instance sekaligus. API WebAssembly.instantiateStreaming melakukannya dengan cara streaming:

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

Jika Anda hanya memerlukan satu instance, tidak ada gunanya menyimpan objek module, sehingga menyederhanakan kode lebih lanjut:

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

Pengoptimalan yang kami terapkan dapat diringkas sebagai berikut:

  • Menggunakan API asinkron untuk menghindari pemblokiran thread utama
  • Menggunakan API streaming untuk mengompilasi dan membuat instance modul WebAssembly dengan lebih cepat
  • Jangan menulis kode yang tidak diperlukan

Selamat bersenang-senang dengan WebAssembly!