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, tạo thực thể cho mô-đun đó rồi sử dụng bất cứ nội dung nào xuất ra trong JavaScript. Bài đăng này giải thích phương pháp 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, tạo thực thể cho mô-đun đó và sau đó sử dụng bất kỳ nội dung nào xuất trong JavaScript. Bài đăng này bắt đầu bằng một mã phổ biến nhưng chưa tối ưu chính xác như vậy, thảo luận 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 vũ đạo tải xuống-biên dịch-tạo thực thể hoàn chỉnh, mặc dù theo cách 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);
})();

Vui lòng 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 đồng bộ, tức là chặn luồng chính cho đến khi hoàn tất. Để ngăn việc sử dụng phần mềm này, Chrome tắt WebAssembly.Module đối với các vùng đệm lớn hơn 4 KB. Để giải quyết giới hạn về kích thước, chúng ta có thể sử dụng await WebAssembly.compile(buffer) thay thế:

(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ẽ tìm hiểu điều đó theo giây.

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 tạo rõ ràng. Trường hợp ngoại lệ duy nhất là new WebAssembly.Instance(module) có cùng bộ đệm 4 KB giới hạn kích thước trong Chrome. Để đảm bảo tính nhất quán và mục đích giữ lại luồng chính miễn phí, nên chúng ta có thể sử dụng thuộc tính không đồng bộ 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);
})();

Hãy quay lại phương pháp tối ưu hoá compile mà tôi gợi ý trước đó. Có chức năng phát trực tuyến biên dịch ứng dụng, thì 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. Từ khi 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 là
lâu hơn thời gian biên dịch của mô-đun WebAssembly, thì WebAssembly.compileStreaming()
kết thúc biên dịch gần như ngay lập tức 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ể truyền Thực thể Response được await fetch(url) trực tiếp 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 Response thực thể. Nếu không cần response ở nơi khác trong mã, bạn có thể chuyển lời hứa được fetch trực tiếp trả về mà không cần await rõ ràng kết quả:

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

Mặc dù vậy, cá nhân tôi thấy bài viết này sẽ dễ đọc hơn nếu để 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? Kết quả là, WebAssembly.instantiate có thể biên dịch và tạo thực thể chỉ trong một thao tác. Chiến lược phát hành đĩa đơ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 phải giữ đối tượng module xung quanh, đơ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ó thể tóm tắt các biện pháp tối ưu hoá mà chúng tôi đã áp dụng 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 thực thể cho các mô-đun WebAssembly nhanh hơn
  • Không viết mã bạn không cần

Hãy giải trí với WebAssembly!