Установка и инициализация wasm-версии

WebAssembly-реализация Esbuild ориентирована на среду, где невозможен или нежелателен запуск нативного бинарника: браузер, ограниченные серверные окружения, песочницы и некоторые serverless-платформы. В отличие от классической версии, основанной на Go-бинарнике, WASM-вариант работает полностью внутри JavaScript-рантайма, используя компилированный модуль .wasm.

Ключевая особенность заключается в том, что производительность остаётся высокой по сравнению с большинством чисто JS-бандлеров, но уступает нативной версии Esbuild из-за ограничений WebAssembly и накладных расходов на передачу данных между JS и WASM.


Установка esbuild-wasm

WASM-версия распространяется как отдельный пакет:

npm install esbuild-wasm

Пакет не содержит нативных бинарников и предназначен для универсального использования в браузере и Node.js (в режиме WASM).

Внутри пакета присутствуют:

  • JavaScript-обвязка (API-слой)
  • WebAssembly-файл сборщика
  • служебные загрузчики для инициализации

Инициализация рантайма WebAssembly

Перед использованием любой функциональности Esbuild необходимо явно запустить и инициализировать WASM-движок. Это ключевое отличие от нативной версии, где бинарник поднимается автоматически.

Базовая инициализация

import * as esbuild from 'esbuild-wasm';

await esbuild.initialize({
  wasmURL: 'https://unpkg.com/esbuild-wasm@0.20.0/esbuild.wasm'
});

Параметры initialize

Основной объект конфигурации определяет способ загрузки и поведение WASM-движка:

  • wasmURL — путь к .wasm файлу. Может быть CDN, локальный сервер или статический ресурс.
  • worker — использование Web Worker для изоляции выполнения (особенно важно в браузере).
  • wasmModule — заранее загруженный бинарный модуль WebAssembly (альтернатива wasmURL).
  • workerURL — кастомный путь к worker-скрипту.

Загрузка WASM-файла

CDN-режим

Самый простой вариант — загрузка с CDN:

await esbuild.initialize({
  wasmURL: 'https://unpkg.com/esbuild-wasm/esbuild.wasm'
});

Этот подход удобен для прототипов, но создаёт зависимость от внешнего ресурса и увеличивает время первого запуска.


Локальная загрузка

Для продакшн-сценариев предпочтительнее локальное размещение:

await esbuild.initialize({
  wasmURL: '/assets/esbuild/esbuild.wasm'
});

WASM-файл размещается рядом со статическими ресурсами приложения и загружается через HTTP-сервер.


Импорт как asset (Vite/Webpack)

В современных сборщиках возможно импортировать WASM как ассет:

import wasmURL from 'esbuild-wasm/esbuild.wasm';

await esbuild.initialize({
  wasmURL
});

Такой подход упрощает управление версиями и кэшированием.


Инициализация с Web Worker

В браузерных приложениях использование Web Worker критично для предотвращения блокировки main thread.

await esbuild.initialize({
  wasmURL: '/esbuild.wasm',
  worker: true
});

При включённом worker:

  • компиляция выполняется вне основного потока
  • UI остаётся отзывчивым
  • снижается риск лагов при сборке больших бандлов

Однократная инициализация

Инициализация должна выполняться один раз на жизненный цикл приложения. Повторный вызов может привести к ошибкам или утечкам ресурсов.

Типичная схема:

let initialized = false;

export async function initEsbuild() {
  if (initialized) return;

  await esbuild.initialize({
    wasmURL: '/esbuild.wasm'
  });

  initialized = true;
}

Проверка готовности среды

Перед вызовом методов build или transform необходимо гарантировать завершение инициализации.

await initEsbuild();

const result = await esbuild.transform(code, {
  loader: 'tsx'
});

Если попытаться вызвать API до инициализации, будет выброшена ошибка о неготовом сервисе.


Работа в браузере

WASM-версия особенно актуальна в браузерных приложениях, где требуется:

  • онлайн-компиляция TypeScript/JSX
  • песочница для пользовательского кода
  • playground-среды
  • визуальные редакторы

Пример минимального окружения:

import * as esbuild from 'esbuild-wasm';

await esbuild.initialize({
  wasmURL: '/esbuild.wasm',
  worker: true
});

const result = await esbuild.transform(
  'const x: number = 1',
  { loader: 'ts' }
);

console.log(result.code);

Особенности загрузки и кеширования WASM

WebAssembly-файл обычно имеет размер в несколько мегабайт, поэтому важны стратегии оптимизации:

HTTP-кеширование

Рекомендуется задавать долгосрочные заголовки:

Cache-Control: public, max-age=31536000, immutable

Lazy loading

Инициализация должна выполняться только при необходимости, например:

  • при открытии редактора
  • при запуске сборки проекта
  • при первом запросе трансформации

Ограничения WASM-версии

Несмотря на функциональную совместимость с нативным Esbuild, существуют ограничения:

  • отсутствие доступа к файловой системе Node.js
  • ограниченная интеграция с native plugins
  • меньшая производительность по сравнению с Go-бинарником
  • необходимость ручного управления загрузкой .wasm
  • зависимость от браузерных API при использовании worker

Использование в Node.js через WASM

Хотя Node.js поддерживает нативную версию Esbuild, WASM-вариант применяется в специфических случаях:

  • изоляция окружения
  • отсутствие нативных бинарников (например, строгие sandbox-платформы)
  • унификация кода между браузером и сервером
import * as esbuild from 'esbuild-wasm';

await esbuild.initialize({
  wasmURL: 'file:///node_modules/esbuild-wasm/esbuild.wasm'
});

Поведение памяти и производительности

WASM-движок использует линейную память, которая:

  • выделяется при инициализации
  • увеличивается при сложных сборках
  • может требовать очистки через перезапуск процесса

При длительной работе в браузере важно учитывать:

  • повторные сборки больших проектов увеличивают потребление памяти
  • отсутствие явного GC внутри WASM-слоя
  • необходимость переинициализации в крайних случаях

Асинхронная модель выполнения

Все операции в WASM-версии строго асинхронные:

  • initialize — загрузка и старт рантайма
  • transform — преобразование кода
  • build — сборка модулей

Это связано с тем, что WebAssembly не может блокировать основной поток JavaScript.

const output = await esbuild.build({
  entryPoints: ['index.ts'],
  bundle: true,
  write: false
});

Организация архитектуры инициализации

В крупных приложениях инициализация Esbuild обычно выносится в отдельный модуль:

// esbuildService.js
import * as esbuild from 'esbuild-wasm';

let servicePromise;

export function getEsbuildService() {
  if (!servicePromise) {
    servicePromise = esbuild.initialize({
      wasmURL: '/esbuild.wasm',
      worker: true
    });
  }
  return servicePromise;
}

Такой подход гарантирует:

  • единый инстанс WASM-движка
  • предотвращение повторной инициализации
  • контроль жизненного цикла сервиса

Подключение через bundler-окружение

В проектах с Vite, Webpack или Rollup важно учитывать обработку .wasm:

  • Vite: поддерживает нативный импорт WASM
  • Webpack: требует настройки experiments.asyncWebAssembly
  • Rollup: необходим WASM plugin

Без корректной настройки загрузка wasmURL может завершиться ошибкой MIME-типа или 404.


Инициализация в изолированных окружениях

При использовании sandbox-сред или iframe:

  • WASM-файл должен быть доступен внутри того же origin
  • worker должен иметь корректный путь
  • запрещены кросс-доменные ограничения без CORS

Практическая модель запуска

Типичный жизненный цикл выглядит следующим образом:

  1. загрузка UI
  2. ленивый импорт esbuild-wasm
  3. загрузка .wasm
  4. инициализация runtime
  5. кэширование инстанса
  6. выполнение transform/build операций
  7. повторное использование без реинициализации

Такой подход минимизирует накладные расходы и обеспечивает стабильную работу даже при частых вызовах компиляции.