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

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

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

Đoạn mã này thực hiện toàn bộ quá trình tải xuống-biên dịch-tạo bản sao, mặc dù không tối ưu:

Đừng sử dụng!

(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) để biến vùng đệm phản hồi thành một mô-đun. Đây là một API đồng bộ, nghĩa là API này sẽ chặn luồng chính cho đến khi hoàn tất. Để hạn 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 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 chưa phải là phương pháp tối ưu, nhưng chúng ta sẽ tìm hiểu về phương pháp này trong giây lát.

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 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 kích thước vùng đệm 4 KB trong Chrome. Để đảm bảo tính nhất quán và giữ cho luồng chính luôn rảnh, 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 với việc tối ưu hoá compile mà tôi đã gợi ý trước đó. Với tính năng biên dịch 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 lời hứa sẽ phân giải thành một thực thể Response. Nếu không cần response ở nơi khác trong mã, bạn có thể trực tiếp truyền lời hứa do fetch trả về mà không cần await kết quả một cách rõ ràng:

(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 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 dễ đọc hơn khi đặt mã này trên 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, sau đó tạo thực thể cho mô-đun đó ngay lập tức? Hóa ra, WebAssembly.instantiate có thể biên dịch và tạo bản sao cùng một lúc. API WebAssembly.instantiateStreaming thực hiện việc này theo phương thức 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ể, thì không cần giữ lại đối tượng module, giúp đơ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 biện pháp tối ưu hoá mà chúng tôi áp dụng có thể được tóm tắt như sau:

  • Sử 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 bản sao nhanh hơn cho các mô-đun WebAssembly
  • Không viết mã bạn không cần

Chúc bạn vui vẻ với WebAssembly!