WebAssembly и TypeScript: когда JavaScript пасует
Вы когда-нибудь чувствовали, что ваш сайт тормозит, хотя написали всё на современном TypeScript? Обработка видео, 3D-рендеринг, сложные математические расчёты — браузерный JavaScript начинает задыхаться. Именно здесь на сцену выходит WebAssembly (или просто Wasm) — технология, которая позволяет запускать в браузере код, написанный на C++, Rust или Go, с почти нативной скоростью. Но как это связано с TypeScript? Спойлер: напрямую — никак, но вместе они образуют мощный тандем.
В этой статье мы не просто дадим сухие определения. Мы покажем, как TypeScript и WebAssembly могут работать в команде, где каждый отвечает за своё: TypeScript — за удобство и типизацию, а Wasm — за производительность. Вы узнаете, как написать простой Wasm-модуль и вызвать его из TypeScript, а главное — поймёте, стоит ли овчинка выделки.
Что такое WebAssembly простыми словами
WebAssembly — это бинарный формат инструкций, который выполняется в виртуальной машине браузера. Если JavaScript — это интерпретируемый язык, который парсится и выполняется "на лету", то Wasm — это уже скомпилированный байт-код. Он загружается быстрее, выполняется почти как нативный код и не даёт браузеру "задумываться".
Ключевые особенности WebAssembly:
- Скорость — до 10-20 раз быстрее JavaScript в вычислительных задачах.
- Безопасность — работает в изолированной среде (sandbox), не имеет прямого доступа к DOM.
- Переносимость — один и тот же .wasm файл работает в Chrome, Firefox, Safari и Edge.
- Не замена JS — а дополнение. DOM, события, асинхронность остаются за JavaScript/TypeScript.
Wasm не предназначен для написания логики интерфейса. Его стихия — тяжёлые вычисления: шифрование, обработка изображений, физические симуляции, игры.
А где тут TypeScript?
Сам TypeScript не компилируется в WebAssembly (хотя есть экспериментальные проекты, но они не для продакшена). Однако TypeScript становится идеальным "клеем" между Wasm-модулями и пользовательским интерфейсом. Представьте: у вас есть быстрый алгоритм на C++, собранный в Wasm. Вы пишете на TypeScript аккуратную обёртку с типами, и ваша IDE начинает подсказывать, какие параметры ожидает функция, какие типы она возвращает. Никакой магии — просто строгость типов и автодополнение.
Вот типичная архитектура:
- Wasm-модуль — чёрный ящик, который делает сложную работу.
- TypeScript-код — загружает модуль, вызывает его функции и обновляет UI.
- Браузер — счастлив, потому что тяжёлая работа не блокирует главный поток.
Пишем пример: генерация простых чисел на WebAssembly и TypeScript
Давайте создадим реальный проект. Мы возьмём алгоритм поиска простых чисел (решето Эратосфена) на C++, скомпилируем его в Wasm, а потом вызовем из TypeScript. Не пугайтесь C++ — код будет простой, а для сборки используем Emscripten.
Шаг 1. Исходный код на C++
Создадим файл primes.cpp:
#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int count_primes(int limit) {
if (limit < 2) return 0;
bool* sieve = new bool[limit + 1];
for (int i = 0; i <= limit; i++) sieve[i] = true;
sieve[0] = sieve[1] = false;
for (int i = 2; i * i <= limit; i++) {
if (sieve[i]) {
for (int j = i * i; j <= limit; j += i) {
sieve[j] = false;
}
}
}
int count = 0;
for (int i = 0; i <= limit; i++) {
if (sieve[i]) count++;
}
delete[] sieve;
return count;
}
}
EMSCRIPTEN_KEEPALIVE — макрос, который говорит компилятору не выкидывать эту функцию при оптимизации.
Шаг 2. Компиляция в Wasm
Установите Emscripten и выполните команду:
emcc primes.cpp -o primes.js -s WASM=1 -s EXPORTED_FUNCTIONS='["_count_primes"]'
На выходе получите два файла: primes.wasm и primes.js — вспомогательный JavaScript-загрузчик.
Шаг 3. TypeScript-обёртка
Теперь самое интересное. Создадим файл wasm-loader.ts:
// Типизируем глобальный объект, который создаёт Emscripten
declare var Module: any;
export async function loadWasmModule(): Promise<any> {
// Используем динамический импорт для загрузки JS-загрузчика
const module = await import('./primes.js');
// Ждём инициализацию Wasm
return new Promise((resolve) => {
Module.onRuntimeInitialized = () => {
resolve(Module);
};
});
}
// Функция-обёртка с типами
export function countPrimes(wasmModule: any, limit: number): number {
if (limit < 2) return 0;
return wasmModule._count_primes(limit);
}
Шаг 4. Использование в React-компоненте (на TypeScript)
import React, { useState, useEffect } from 'react';
import { loadWasmModule, countPrimes } from './wasm-loader';
const PrimeCounter: React.FC = () => {
const [wasmReady, setWasmReady] = useState(false);
const [wasmModule, setWasmModule] = useState<any>(null);
const [limit, setLimit] = useState(100000);
const [result, setResult] = useState<number | null>(null);
useEffect(() => {
loadWasmModule().then(mod => {
setWasmModule(mod);
setWasmReady(true);
});
}, []);
const handleCalculate = () => {
if (wasmModule) {
const start = performance.now();
const primes = countPrimes(wasmModule, limit);
const end = performance.now();
setResult(primes);
console.log(`Время выполнения: ${(end - start).toFixed(2)} мс`);
}
};
return (
<div>
<h3>Поиск простых чисел через WebAssembly</h3>
<input
type="number"
value={limit}
onChange={(e) => setLimit(Number(e.target.value))}
/>
<button onClick={handleCalculate} disabled={!wasmReady}>
{wasmReady ? 'Посчитать' : 'Загрузка Wasm...'}
</button>
{result !== null && <p>Найдено простых чисел: {result}</p>}
</div>
);
};
export default PrimeCounter;
Заметьте: TypeScript да