Tải hiệu quả các mô-đun WebAssembly

Khi làm việc với WebAssembly, bạn thường nên tải xuống một mô-đun, biên dịch, tạo bản sao mô-đun và sau đó sử dụng bất kỳ mô-đun nào xuất trong JavaScript. Bài đăng này giải thích phương pháp chúng tôi đề xuất để đạt hiệu quả tối ưu.

Khi làm việc với WebAssembly, bạn thường muốn tải xuống một mô-đun, biên dịch, tạo bản sao mô-đun, sau đó sử dụng bất kỳ mô-đun nào xuất trong JavaScript. Bài đăng này bắt đầu bằng một đoạn mã phổ biến nhưng dưới mức tối ưu, thực hiện chính xác điều đó, thảo luận về một số cách tối ưu hoá có thể có và cuối cùng cho thấy cách đơn giản, hiệu quả nhất để chạy WebAssembly từ JavaScript.

Đoạn mã này thực hiện điệu nhảy tải-biên dịch-trình tạo hoàn chỉnh, mặc dù theo cách chưa tối ưu:

Đừng dùng tính năng này!

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

Lưu ý cách chúng ta sử dụng new WebAssembly.Module(buffer) để chuyển vùng đệm phản hồi thành một mô-đun. Đây là một API đồng bộ, có nghĩa là API này chặn luồng chính cho đến khi hoàn tất. Để không khuyến khích việc sử dụng, Chrome sẽ tắt WebAssembly.Module đối với các vùng đệm lớn hơn 4 KB. Để khắc phục giới hạn về kích thước, chúng ta có thể sử dụng 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) vẫn không phải là phương pháp tối ưu, nhưng chúng ta sẽ đi sâu vào điều đó trong một giây nữa.

Hầu hết mọi thao tác trong đoạn mã được sửa đổi hiện không đồng bộ, vì việc sử dụng await sẽ làm rõ ràng. Trường hợp ngoại lệ duy nhất là new WebAssembly.Instance(module) có cùng giới hạn dung lượng bộ nhớ đệm 4 KB trong Chrome. Để đảm bảo tính nhất quán và nhằm giữ cho luồng chính không có hoạt động, chúng ta có thể sử dụng WebAssembly.instantiate(module) không đồng bộ.

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

Hãy quay lại tính năng tối ưu hoá compile mà tôi có gợi ý trước đó. Với tính năng biên dịch phát trực tuyến, trình duyệt có thể bắt đầu biên dịch mô-đun WebAssembly trong khi các byte mô-đun vẫn đang tải xuống. Vì quá trình tải xuống và biên dịch diễn ra song song nên quá trình này sẽ nhanh hơn – đặc biệt là đối với các tải trọng lớn.

Khi thời gian tải xuống dài hơn thời gian biên dịch của mô-đun WebAssembly, thì WebAssembly.compileStreaming()
sẽ hoàn tất quá trình biên dịch gần như ngay sau khi các byte cuối cùng được tải xuống.

Để bật tính năng tối ưu hoá này, hãy sử dụng WebAssembly.compileStreaming thay vì WebAssembly.compile. Thay đổi này cũng cho phép chúng ta loại bỏ vùng đệm mảng trung gian, vì giờ đây, chúng ta có thể trực tiếp truyền thực thể Response do await fetch(url) trả về.

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

API WebAssembly.compileStreaming cũng chấp nhận một lời hứa phân giải tới một thực thể Response. Nếu không cần response ở nơi nào khác trong mã, bạn có thể truyền trực tiếp lời hứa được fetch trả về mà không cần await rõ ràng kết quả của nó:

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

Nếu không cần kết quả fetch ở nơi nào khác, bạn thậm chí có thể truyền trực tiếp kết quả đó:

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

Tuy nhiên, cá nhân tôi thấy tài liệu này dễ đọc hơn nếu tách thành một dòng riêng.

Xem cách chúng ta biên dịch phản hồi thành một mô-đun và tạo thực thể mô-đun ngay lập tức? Thực tế, WebAssembly.instantiate có thể biên dịch và tạo thực thể trong một lần. API WebAssembly.instantiateStreaming thực hiện việc này theo cách truyền trực tuyến:

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

Nếu bạn chỉ cần một thực thể duy nhất thì không cần thiết phải giữ lại đối tượng module, hãy đơn giản hoá mã hơn nữa:

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

Các tối ưu hoá chúng tôi đã áp dụng có thể được tóm tắt như sau:

  • Dùng API không đồng bộ để tránh chặn luồng chính
  • Sử dụng API truyền trực tuyến để biên dịch và tạo thực thể cho các mô-đun WebAssembly nhanh hơn
  • Không viết mã mà bạn không cần

Hãy vui vẻ với WebAssembly!