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 kami rekomendasikan untuk efisiensi optimal.

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

Cuplikan kode ini menjalankan tarian download-compile-instantiate lengkap, meskipun dengan cara yang kurang optimal:

Jangan menggunakannya!

(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 kita menggunakan new WebAssembly.Module(buffer) untuk mengubah buffer respons menjadi modul. Ini adalah API sinkron, artinya 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 segera membahasnya.

Hampir setiap operasi dalam cuplikan yang diubah kini bersifat asinkron, karena penggunaan await memperjelas. Satu-satunya pengecualian adalah new WebAssembly.Instance(module), yang memiliki batasan ukuran buffer 4 KB yang sama di Chrome. Demi konsistensi dan demi menjaga 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 petunjukkan 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 daripada waktu kompilasi modul WebAssembly, WebAssembly.compileStreaming() segera menyelesaikan kompilasi segera setelah byte terakhir didownload.

Untuk mengaktifkan pengoptimalan ini, gunakan WebAssembly.compileStreaming, bukan WebAssembly.compile. Perubahan ini juga memungkinkan kita untuk menghilangkan buffer 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 tidak memerlukan response di bagian lain dalam kode, Anda dapat meneruskan promise yang ditampilkan oleh fetch secara langsung, tanpa melakukan await pada 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 Anda juga 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);
})();

Tapi saya pribadi merasa lebih mudah dibaca untuk memisahkannya dalam baris terpisah.

Lihat cara kita mengompilasi respons ke dalam modul, lalu langsung membuat instance? Ternyata, WebAssembly.instantiate dapat mengompilasi dan membuat instance dalam sekali jalan. WebAssembly.instantiateStreaming API melakukan hal ini secara 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 kita 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 Anda perlukan

Selamat bersenang-senang dengan WebAssembly!