При работе с 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, то есть он блокирует основной поток до его завершения. Чтобы препятствовать его использованию, Chrome отключает WebAssembly.Module
для буферов размером более 4 КБ. Чтобы обойти ограничение размера, мы можем вместо этого использовать 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 КБ в 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.compileStreaming
вместо WebAssembly.compile
. Это изменение также позволяет нам избавиться от буфера промежуточного массива, поскольку теперь мы можем напрямую передать экземпляр Response
, возвращаемый функцией await fetch(url)
.
(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
также принимает обещание, которое разрешается в экземпляр 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
может компилироваться и создавать экземпляры за один раз. API WebAssembly.instantiateStreaming
делает это потоковым способом:
(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!