Реализация WebAssembly (WASM) модулей для веб-приложения
WebAssembly — бинарный формат инструкций для виртуальной машины, встроенной в браузер. Не замена JavaScript — дополнение для задач, где нативная скорость критична: кодеки, криптография, обработка изображений, физические движки, ML-инференс, CAD-инструменты, эмуляторы.
WASM работает в изолированной sandbox, разделяет память с JS через WebAssembly.Memory (линейный буфер ArrayBuffer), вызывается из JS как обычная функция.
Три пути получить WASM-модуль
1. Компиляция из Rust (предпочтительно)
Rust + wasm-pack — лучший опыт разработки на сегодня. Хорошая типизация, автоматический биндинг через wasm-bindgen, поддержка сложных типов.
cargo new --lib image-processor
cd image-processor
cargo add wasm-bindgen image
// src/lib.rs
use wasm_bindgen::prelude::*;
use image::{DynamicImage, ImageFormat};
use std::io::Cursor;
#[wasm_bindgen]
pub fn resize_image(data: &[u8], width: u32, height: u32) -> Vec<u8> {
let img = image::load_from_memory(data).unwrap();
let resized = img.resize_exact(width, height, image::imageops::FilterType::Lanczos3);
let mut output = Cursor::new(Vec::new());
resized.write_to(&mut output, ImageFormat::WebP).unwrap();
output.into_inner()
}
#[wasm_bindgen]
pub fn grayscale(data: &[u8]) -> Vec<u8> {
let img = image::load_from_memory(data).unwrap();
let gray = img.grayscale();
let mut output = Cursor::new(Vec::new());
gray.write_to(&mut output, ImageFormat::Png).unwrap();
output.into_inner()
}
wasm-pack build --target web --release
2. Компиляция из C/C++ через Emscripten
Для портирования существующих C-библиотек:
emcc processing.c \
-O3 \
-o processing.js \
-s WASM=1 \
-s EXPORTED_FUNCTIONS='["_process_data"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-s ALLOW_MEMORY_GROWTH=1
3. Готовые WASM-пакеты
Многие библиотеки уже поставляют WASM: @ffmpeg/ffmpeg, @sqlite.org/sqlite-wasm, pdfium-wasm, @mlc-ai/web-llm.
Загрузка и инициализация
// wasm-loader.ts
interface WasmModule {
resize_image: (data: Uint8Array, width: number, height: number) => Uint8Array
grayscale: (data: Uint8Array) => Uint8Array
memory: WebAssembly.Memory
}
let moduleInstance: WasmModule | null = null
async function loadWasmModule(): Promise<WasmModule> {
if (moduleInstance) return moduleInstance
// Streaming compilation — быстрее чем ArrayBuffer
const response = await fetch('/wasm/image-processor.wasm')
const { instance } = await WebAssembly.instantiateStreaming(response, {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 4096 }),
},
})
moduleInstance = instance.exports as unknown as WasmModule
return moduleInstance
}
// Или через wasm-pack generated bindings
async function loadRustWasm() {
const { default: init, resize_image, grayscale } = await import('./pkg/image_processor')
await init() // загружает и инициализирует WASM
return { resize_image, grayscale }
}
Передача данных: JS ↔ WASM
WASM работает только с числами (i32, i64, f32, f64). Строки и массивы требуют работы с памятью:
// Ручная работа с памятью (без wasm-bindgen)
async function callWasmWithData(
wasmModule: WebAssembly.Instance,
inputData: Uint8Array
): Promise<Uint8Array> {
const exports = wasmModule.exports as {
alloc: (size: number) => number
dealloc: (ptr: number, size: number) => void
process: (ptr: number, len: number) => number
memory: WebAssembly.Memory
}
const memory = new Uint8Array(exports.memory.buffer)
// Выделить память в WASM и скопировать данные
const inputPtr = exports.alloc(inputData.length)
memory.set(inputData, inputPtr)
// Вызвать функцию — она вернёт указатель на результат
const resultPtr = exports.process(inputPtr, inputData.length)
// Прочитать длину результата (первые 4 байта — конвенция)
const resultLen = new DataView(exports.memory.buffer).getUint32(resultPtr, true)
const result = new Uint8Array(exports.memory.buffer, resultPtr + 4, resultLen).slice()
// Освободить память
exports.dealloc(inputPtr, inputData.length)
exports.dealloc(resultPtr, resultLen + 4)
return result
}
С wasm-pack/wasm-bindgen этот boilerplate генерируется автоматически.
Запуск WASM в Web Worker
Тяжёлые WASM-операции нужно выносить в Worker, иначе main thread блокируется:
// wasm-worker.ts
import init, { resize_image } from './pkg/image_processor'
let initialized = false
self.onmessage = async (event: MessageEvent) => {
const { id, type, payload } = event.data
if (!initialized) {
await init()
initialized = true
}
try {
switch (type) {
case 'RESIZE': {
const { imageData, width, height } = payload
const result = resize_image(new Uint8Array(imageData), width, height)
// Transferable — без копирования
self.postMessage(
{ id, type: 'RESULT', payload: result.buffer },
[result.buffer]
)
break
}
}
} catch (error) {
self.postMessage({ id, type: 'ERROR', payload: (error as Error).message })
}
}
// main.ts — использование
const worker = new Worker(new URL('./wasm-worker.ts', import.meta.url), {
type: 'module',
})
async function resizeImage(file: File, width: number, height: number): Promise<Blob> {
const buffer = await file.arrayBuffer()
return new Promise((resolve, reject) => {
const id = Math.random().toString(36).slice(2)
const handler = (event: MessageEvent) => {
if (event.data.id !== id) return
worker.removeEventListener('message', handler)
if (event.data.type === 'ERROR') {
reject(new Error(event.data.payload))
} else {
resolve(new Blob([event.data.payload], { type: 'image/webp' }))
}
}
worker.addEventListener('message', handler)
// Transferable — передаём буфер без копирования
worker.postMessage({ id, type: 'RESIZE', payload: { imageData: buffer, width, height } }, [buffer])
})
}
Пример: SQLite в браузере
import { createDbWorker } from 'sql.js-httpvfs'
async function initDatabase() {
const worker = await createDbWorker(
[{ from: 'inline', config: { serverMode: 'full', url: '/db/products.sqlite3', requestChunkSize: 4096 } }],
'/sqlite.worker.js',
'/sql-wasm.wasm'
)
const results = await worker.db.query(
'SELECT id, name, price FROM products WHERE category = ? ORDER BY price LIMIT 20',
['electronics']
)
return results
}
Оптимизации загрузки
<!-- Предзагрузка WASM -->
<link rel="preload" href="/wasm/processor.wasm" as="fetch" type="application/wasm" crossorigin>
// Кеш скомпилированного модуля через Cache API
async function loadWithCache(url: string): Promise<WebAssembly.Module> {
const cache = await caches.open('wasm-modules-v1')
const cached = await cache.match(url)
if (cached) {
const buffer = await cached.arrayBuffer()
return WebAssembly.compile(buffer)
}
const response = await fetch(url)
await cache.put(url, response.clone())
return WebAssembly.compileStreaming(response)
}
Что входит в работу
Выбор языка компиляции (Rust/C/готовый пакет) под задачу, настройка сборки wasm-pack или Emscripten, реализация JS-биндингов, интеграция с Web Worker для неблокирующего выполнения, настройка HTTP-заголовков (Content-Type: application/wasm, COEP/COOP для SharedArrayBuffer), оптимизация размера бинарника.
WASM-бинарники требуют дополнительных HTTP-заголовков для SharedArrayBuffer:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Срок: 3–5 дней в зависимости от сложности алгоритма и необходимости писать Rust/C код с нуля.







