การโหลดโมดูล WebAssembly อย่างมีประสิทธิภาพ

เมื่อทำงานกับ WebAssembly คุณมักต้องการดาวน์โหลดโมดูล คอมไพล์ สร้างอินสแตนซ์ จากนั้นใช้สิ่งใดก็ตามที่ส่งออกไปใน JavaScript โพสต์นี้อธิบายวิธีที่เราแนะนำเพื่อประสิทธิภาพสูงสุด

เมื่อทำงานกับ WebAssembly คุณมักต้องการดาวน์โหลดโมดูล คอมไพล์ สร้างอินสแตนซ์ และ จากนั้นใช้อะไรก็ตามที่ส่งออกเป็น JavaScript โพสต์นี้เริ่มต้นด้วยโค้ดที่ใช้กันโดยทั่วไปแต่มีประสิทธิภาพต่ำกว่ามาตรฐาน กล่าวถึงการเพิ่มประสิทธิภาพที่เป็นไปได้หลายๆ แบบ และสุดท้ายจะแสดง วิธีที่ง่ายและมีประสิทธิภาพที่สุดในการเรียกใช้ WebAssembly จาก JavaScript

ข้อมูลโค้ดนี้ทำให้การดาวน์โหลด-คอมไพล์ ไม่หยุดนิ่ง แม้ว่าจะเป็นการกระทำที่ไม่เหมาะสมก็ตาม:

ห้ามใช้

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

อย่าลืมสังเกตวิธีที่เราใช้ new WebAssembly.Module(buffer) เพื่อเปลี่ยนบัฟเฟอร์การตอบกลับให้เป็นโมดูล นี่คือ API แบบซิงโครนัส ซึ่งหมายความว่า API จะบล็อกเทรดหลักจนกว่าจะเสร็จสมบูรณ์ เพื่อลดการใช้งาน Chrome ปิดใช้ WebAssembly.Module สำหรับบัฟเฟอร์ที่มีขนาดใหญ่กว่า 4 KB ในการแก้ปัญหาขนาดสูงสุด เราสามารถ ใช้ 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) ยังยังไม่ใช่แนวทางที่ดีที่สุด แต่เราจะพูดถึงเรื่องนี้ อย่างที่สอง

ปัจจุบันการดำเนินการเกือบทุกอย่างในข้อมูลโค้ดที่แก้ไขจะเป็นแบบไม่พร้อมกัน เนื่องจากการใช้ await ล้าง ข้อยกเว้นเพียงอย่างเดียวคือ new WebAssembly.Instance(module) ซึ่งมีบัฟเฟอร์ 4 KB เท่ากัน การจำกัดขนาดใน Chrome เพื่อความสอดคล้องและเพื่อเก็บเทรดหลักไว้ ฟรี เราก็สามารถใช้ฟังก์ชัน แบบอะซิงโครนัส 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);
})();

กลับมาที่การเพิ่มประสิทธิภาพ compile ที่ฉันบอกใบ้ก่อนหน้านี้กัน มีสตรีมมิง คอมไพล์ เบราว์เซอร์สามารถ ให้เริ่มคอมไพล์โมดูล WebAssembly ในขณะที่ไบต์ของโมดูลยังดาวน์โหลดอยู่ ตั้งแต่ดาวน์โหลด และการคอมไพล์จะดำเนินการควบคู่กันไปด้วย การดำเนินการนี้จะเร็วกว่า โดยเฉพาะสำหรับเพย์โหลดขนาดใหญ่

เมื่อเวลาในการดาวน์โหลดคือ
นานกว่าเวลาคอมไพล์ของโมดูล WebAssembly ตามด้วย WebAssembly.compileStreaming()
การคอมไพล์จะเสร็จสิ้นเกือบจะทันทีหลังจากที่ดาวน์โหลดไบต์สุดท้าย

หากต้องการเปิดใช้การเพิ่มประสิทธิภาพนี้ ให้ใช้ WebAssembly.compileStreaming แทน WebAssembly.compile การเปลี่ยนแปลงนี้ยังช่วยให้เราสามารถกำจัดบัฟเฟอร์อาร์เรย์ขั้นกลาง เนื่องจากขณะนี้เราสามารถส่งผ่าน await fetch(url) ส่งกลับอินสแตนซ์ Response โดยตรง

(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 ยังยอมรับคำมั่นสัญญาที่แก้ไขให้เป็น Response อินสแตนซ์ หากคุณไม่ใช้ response ในส่วนอื่นในโค้ด ก็ส่งคำสัญญาได้ ส่งคืนโดย fetch โดยตรง โดยไม่awaitแสดงผลลัพธ์อย่างชัดเจน:

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

หากไม่ต้องการผลลัพธ์ fetch จากที่อื่น คุณก็ส่งไปโดยตรงได้เช่นกัน

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

แต่โดยส่วนตัวแล้ว ฉันพบว่าการแบ่งออกเป็นอีกบรรทัดหนึ่งอ่านได้ง่ายกว่า

ดูวิธีที่เรารวบรวมคำตอบลงในโมดูล แล้วสร้างตัวอย่างทันที ผลที่ได้คือ WebAssembly.instantiate คอมไพล์และสร้างอินสแตนซ์ได้ในครั้งเดียว WebAssembly.instantiateStreaming API จะดำเนินการในลักษณะของการสตรีม:

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

หากคุณต้องการเพียงอินสแตนซ์เดียว ก็ไม่มีประโยชน์ที่จะเก็บออบเจ็กต์ module ไว้ การลดความซับซ้อนของโค้ดเพิ่มเติม

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

การเพิ่มประสิทธิภาพที่เรานำไปใช้สามารถสรุปได้ดังนี้

  • ใช้ API แบบอะซิงโครนัสเพื่อหลีกเลี่ยงการบล็อกเทรดหลัก
  • ใช้ API การสตรีมเพื่อคอมไพล์และสร้างอินสแตนซ์โมดูล WebAssembly ได้เร็วขึ้น
  • อย่าเขียนโค้ดที่ไม่จำเป็น

ขอให้สนุกกับ WebAssembly