Memuat modul WebAssembly secara efisien

Saat bekerja dengan WebAssembly, Anda sering kali ingin mendownload modul, mengompilasinya, membuat instance, dan kemudian menggunakan apa pun yang diekspornya dalam JavaScript. Postingan ini menjelaskan pendekatan yang kami rekomendasikan untuk efisiensi optimal.

Saat bekerja dengan WebAssembly, Anda sering kali ingin mengunduh modul, mengompilasinya, membuat instance, dan kemudian menggunakan apa pun yang diekspornya di JavaScript. Postingan ini dimulai dengan kode yang umum tetapi kurang optimal yang melakukan hal tersebut, membahas beberapa kemungkinan pengoptimalan, dan pada akhirnya menunjukkan cara paling sederhana dan efisien untuk menjalankan WebAssembly dari JavaScript.

Cuplikan kode ini melakukan dance download-compile-instanceiate yang 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 kita menggunakan new WebAssembly.Module(buffer) untuk mengubah buffer respons menjadi modul. Ini adalah 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 bisa gunakan await WebAssembly.compile(buffer) sebagai gantinya:

(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 mencapainya dalam kedua.

Hampir setiap operasi dalam cuplikan yang dimodifikasi sekarang asinkron, karena penggunaan await membuat jelas lagi. Satu-satunya pengecualian adalah new WebAssembly.Instance(module), yang memiliki buffer 4 KB yang sama pembatasan ukuran di Chrome. Untuk konsistensi dan demi mempertahankan thread utama gratis, kita bisa menggunakan model 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);
})();

Mari kembali ke pengoptimalan compile yang saya petunjuk sebelumnya. Dengan streaming kompilasi, browser sudah dapat mulai mengompilasi modul WebAssembly selagi byte modul masih diunduh. Sejak download dan kompilasi terjadi secara paralel, ini lebih cepat — terutama untuk payload besar.

Saat waktu download
lebih lama dari waktu kompilasi modul WebAssembly, lalu WebAssembly.compileStreaming()
menyelesaikan kompilasi hampir segera setelah byte terakhir diunduh.

Untuk mengaktifkan pengoptimalan ini, gunakan WebAssembly.compileStreaming, bukan WebAssembly.compile. Perubahan ini juga memungkinkan kita menghapus buffer array perantara, karena sekarang kita bisa 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 menjadi Response di instance Compute Engine. Jika response tidak diperlukan di bagian lain dalam kode, Anda dapat meneruskan promise 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 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);
})();

Namun, saya pribadi merasa lebih mudah membaca bila menulisnya di baris terpisah.

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

Selamat bersenang-senang dengan WebAssembly!