Введение в WebAssembly и его роль в Node.js
WebAssembly (WASM) — это низкоуровневый бинарный формат инструкций, который выполняется в виртуальной машине с почти нативной производительностью. В экосистеме Node.js WebAssembly открывает новые горизонты: позволяет выполнять код, написанный на C, C++, Rust или Go, прямо в серверной среде. Это особенно актуально для задач, требующих высокой производительности: обработка изображений, криптография, парсинг данных, работа с большими массивами чисел.
В этой статье мы разберем полный цикл работы с WebAssembly в Node.js: от компиляции исходного кода до интеграции готового модуля в JavaScript-приложение.
Основные понятия и подготовка окружения
Что нужно знать перед началом
WebAssembly модуль — это бинарный файл с расширением .wasm. Он не имеет доступа к DOM или файловой системе напрямую — все взаимодействие происходит через "импортируемые функции", которые вы передаете из JavaScript. В Node.js это ограничение снимается с помощью специальных оберток.
Установка инструментов
Для компиляции кода в WASM нам понадобится Emscripten SDK. Установка на Linux/macOS:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
Для Rust-проектов используйте wasm-pack:
npm install -g wasm-pack
Создание и компиляция первого WASM-модуля
Пример на C — функция сложения
Создадим простейшую функцию на C, которая складывает два числа:
// math.c
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
int multiply(int a, int b) {
return a * b;
}
Компилируем в WASM с помощью Emscripten:
emcc math.c -o math.js -s EXPORTED_FUNCTIONS="['_add', '_multiply']" -s WASM=1 -s MODULARIZE=1 -s EXPORT_NAME="createMathModule"
На выходе получаем два файла: math.wasm и math.js — обертку для загрузки.
Загрузка модуля в Node.js
Теперь используем модуль в JavaScript:
// index.js
const createMathModule = require('./math.js');
createMathModule().then(module => {
const add = module._add;
const multiply = module._multiply;
console.log('5 + 3 =', add(5, 3)); // 8
console.log('4 * 7 =', multiply(4, 7)); // 28
});
Запускаем:
node index.js
Работа с памятью и сложными типами данных
Передача строк и массивов
WASM оперирует только числами. Для передачи строк или массивов нужно выделять память в линейной памяти модуля. Рассмотрим пример на C, который принимает строку и возвращает ее длину:
// string_ops.c
#include <string.h>
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int string_length(char* str) {
return strlen(str);
}
EMSCRIPTEN_KEEPALIVE
void to_upper(char* str) {
for (int i = 0; str[i]; i++) {
if (str[i] >= 'a' && str[i] <= 'z') {
str[i] -= 32;
}
}
}
Компилируем:
emcc string_ops.c -o string_ops.js -s EXPORTED_FUNCTIONS="['_string_length', '_to_upper']" -s WASM=1 -s MODULARIZE=1 -s EXPORT_NAME="createStringModule"
JavaScript-код для работы с памятью:
const createStringModule = require('./string_ops.js');
createStringModule().then(module => {
const { _string_length, _to_upper, heap } = module;
// Выделяем память под строку
const input = "hello world";
const ptr = module._malloc(input.length + 1);
// Копируем строку в память WASM
for (let i = 0; i < input.length; i++) {
heap[i + ptr] = input.charCodeAt(i);
}
heap[ptr + input.length] = 0; // null-terminator
console.log('Длина строки:', _string_length(ptr)); // 11
_to_upper(ptr);
let result = '';
for (let i = ptr; heap[i] !== 0; i++) {
result += String.fromCharCode(heap[i]);
}
console.log('Результат:', result); // HELLO WORLD
module._free(ptr); // Освобождаем память
});
Оптимизация производительности и best practices
Измерение производительности
Сравним выполнение чисто JavaScript-функции и WASM-функции на задаче вычисления чисел Фибоначчи:
// fib.c
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
Тест в Node.js:
// test_fib.js
const createFibModule = require('./fib.js');
// JavaScript версия
function fibJS(n) {
if (n <= 1) return n;
return fibJS(n-1) + fibJS(n-2);
}
async function test() {
const module = await createFibModule();
const fibWASM = module._fib;
console.time('WASM fib(40)');
console.log(fibWASM(40));
console.timeEnd('WASM fib(40)');
console.time('JS fib(40)');
console.log(fibJS(40));
console.timeEnd('JS fib(40)');
}
test();
Результат может показать прирост скорости в 2-5 раз в зависимости от задачи.
Советы по оптимизации
- Минимизируйте переходы между JS и WASM — каждый вызов имеет накладные расходы
- Используйте буферы для передачи больших объемов данных вместо отдельных значений
- Компилируйте с оптимизациями: добавьте флаг
-O3в Emscripten - Кешируйте скомпилированные модули с помощью
WebAssembly.compileStreaming() - Избегайте глобальных переменных