Впровадження 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 згенеровані біндинги
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 код з нуля.







