React с WebAssembly: интеграция Wasm-модулей

16 октября 2026
Автор

Антон Ларичев

WebAssembly (Wasm) — бинарный формат инструкций, который выполняется в браузере почти с нативной скоростью. Когда JavaScript не справляется с тяжёлыми вычислениями — обработкой изображений, криптографией, физическими симуляциями, разбором форматов — WebAssembly становится естественным дополнением. В этой статье разберём, как интегрировать Wasm-модули в React-приложение: от компиляции до удобных хуков.

Когда стоит использовать WebAssembly в React

WebAssembly не заменяет JavaScript — он дополняет его там, где скорость критична:

  • Математические вычисления (FFT, матричные операции, статистика)
  • Обработка медиа в браузере (кодеки, фильтры изображений)
  • Криптография и хэширование
  • Разбор бинарных форматов (PDF, DICOM, геоданные)
  • Портирование нативных библиотек на C/C++/Rust

Для обычной бизнес-логики, работы с DOM и сетевых запросов JavaScript подходит лучше: меньше накладных расходов на маршаллинг данных.

Курс по теме

Курс React и Redux Toolkit с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Компиляция Wasm-модуля

Вариант 1: Rust через wasm-pack

Rust — наиболее удобный путь к WebAssembly благодаря инструментарию wasm-pack.

Установите инструменты:

curl https://sh.rustup.rs -sSf | sh
cargo install wasm-pack

Создайте библиотеку:

cargo new --lib wasm-math
cd wasm-math

Отредактируйте Cargo.toml:

[package]
name = "wasm-math"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Напишите функции в src/lib.rs:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
    if n <= 1 {
        return n as u64;
    }
    let mut a: u64 = 0;
    let mut b: u64 = 1;
    for _ in 2..=n {
        let tmp = a + b;
        a = b;
        b = tmp;
    }
    b
}

#[wasm_bindgen]
pub fn sum_array(data: &[f64]) -> f64 {
    data.iter().sum()
}

Скомпилируйте:

wasm-pack build --target web --out-dir pkg

В директории pkg появятся .wasm-файл и JS-обёртка с TypeScript-типами.

Вариант 2: C/C++ через Emscripten

Для существующих C-библиотек используется Emscripten.

// math_lib.c
#include <emscripten/emscripten.h>

EMSCRIPTEN_KEEPALIVE
int fast_multiply(int a, int b) {
    return a * b;
}
emcc math_lib.c -o math_lib.js \
  -s EXPORTED_FUNCTIONS='["_fast_multiply"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
  -s MODULARIZE=1 \
  -s EXPORT_NAME=MathLib

Структура React-проекта с Wasm

Поместите скомпилированные файлы в проект:

src/
  wasm/
    wasm_math.wasm
    wasm_math.js       # JS-обёртка от wasm-pack
    wasm_math.d.ts     # TypeScript-типы
  hooks/
    useWasm.ts
    useMath.ts
  components/
    FibCalculator.tsx
public/
  wasm/
    wasm_math_bg.wasm  # бинарный файл должен быть доступен по HTTP

При работе с Create React App или Vite скопируйте .wasm-файл в public/, чтобы браузер мог загрузить его напрямую.

Загрузка Wasm-модуля: базовый подход

Brower API для работы с Wasm — WebAssembly.instantiateStreaming. Он загружает и компилирует модуль за один шаг.

// src/wasm/loader.ts
export async function loadWasmModule(url: string) {
  const response = await fetch(url);
  const { instance } = await WebAssembly.instantiateStreaming(response);
  return instance.exports;
}

Для модулей, собранных через wasm-pack, используйте сгенерированную JS-обёртку:

import init, { fibonacci, sum_array } from './wasm/wasm_math';

async function bootstrap() {
  // init загружает .wasm и инициализирует память
  await init('/wasm/wasm_math_bg.wasm');
  
  const result = fibonacci(40);
  console.log(result); // 102334155
}

Универсальный хук useWasm

Создадим хук, который инкапсулирует жизненный цикл загрузки модуля:

// src/hooks/useWasm.ts
import { useState, useEffect, useRef } from 'react';

type WasmStatus = 'idle' | 'loading' | 'ready' | 'error';

interface UseWasmResult<T> {
  module: T | null;
  status: WasmStatus;
  error: Error | null;
}

export function useWasm<T>(
  initializer: () => Promise<T>
): UseWasmResult<T> {
  const [module, setModule] = useState<T | null>(null);
  const [status, setStatus] = useState<WasmStatus>('idle');
  const [error, setError] = useState<Error | null>(null);
  const initialized = useRef(false);

  useEffect(() => {
    if (initialized.current) return;
    initialized.current = true;

    setStatus('loading');
    initializer()
      .then((mod) => {
        setModule(mod);
        setStatus('ready');
      })
      .catch((err) => {
        setError(err instanceof Error ? err : new Error(String(err)));
        setStatus('error');
      });
  }, []);

  return { module, status, error };
}

Специализированный хук для конкретного модуля:

// src/hooks/useMath.ts
import { useWasm } from './useWasm';
import init, { fibonacci, sum_array } from '../wasm/wasm_math';

interface MathModule {
  fibonacci: (n: number) => bigint;
  sumArray: (data: Float64Array) => number;
}

export function useMathWasm() {
  return useWasm<MathModule>(async () => {
    await init('/wasm/wasm_math_bg.wasm');
    return {
      fibonacci,
      sumArray: (data: Float64Array) => sum_array(data),
    };
  });
}

Компоненты, использующие Wasm

Простой калькулятор Фибоначчи

// src/components/FibCalculator.tsx
import { useState } from 'react';
import { useMathWasm } from '../hooks/useMathWasm';

export function FibCalculator() {
  const { module, status } = useMathWasm();
  const [input, setInput] = useState(10);
  const [result, setResult] = useState<bigint | null>(null);
  const [elapsed, setElapsed] = useState<number | null>(null);

  const calculate = () => {
    if (!module) return;
    const start = performance.now();
    const fib = module.fibonacci(input);
    const end = performance.now();
    setResult(fib);
    setElapsed(end - start);
  };

  if (status === 'loading') return <p>Загрузка WebAssembly...</p>;
  if (status === 'error') return <p>Ошибка загрузки модуля</p>;

  return (
    <div>
      <h2>Вычисление числа Фибоначчи</h2>
      <input
        type="number"
        value={input}
        min={0}
        max={80}
        onChange={(e) => setInput(Number(e.target.value))}
      />
      <button onClick={calculate} disabled={status !== 'ready'}>
        Вычислить
      </button>
      {result !== null && (
        <p>
          F({input}) = {result.toString()}
          {elapsed !== null && ` (${elapsed.toFixed(3)} мс)`}
        </p>
      )}
    </div>
  );
}

Обработка больших массивов

Передача массивов в Wasm требует использования типизированных массивов. wasm-bindgen автоматически создаёт нужные обёртки:

// src/components/ArrayProcessor.tsx
import { useState, useCallback } from 'react';
import { useMathWasm } from '../hooks/useMathWasm';

export function ArrayProcessor() {
  const { module, status } = useMathWasm();
  const [result, setResult] = useState<number | null>(null);

  const processLargeArray = useCallback(() => {
    if (!module) return;

    // Создаём типизированный массив — Wasm работает только с ними
    const data = new Float64Array(1_000_000);
    for (let i = 0; i < data.length; i++) {
      data[i] = Math.random();
    }

    const sum = module.sumArray(data);
    setResult(sum);
  }, [module]);

  return (
    <div>
      <h2>Суммирование 1 млн элементов</h2>
      <button onClick={processLargeArray} disabled={status !== 'ready'}>
        Обработать
      </button>
      {result !== null && <p>Сумма: {result.toFixed(4)}</p>}
    </div>
  );
}

Паттерн: Wasm в Web Worker

Для очень тяжёлых вычислений имеет смысл вынести Wasm в Web Worker, чтобы не блокировать главный поток:

// src/workers/math.worker.ts
import init, { fibonacci } from '../wasm/wasm_math';

let initialized = false;

async function ensureInit() {
  if (!initialized) {
    await init('/wasm/wasm_math_bg.wasm');
    initialized = true;
  }
}

self.onmessage = async (event: MessageEvent) => {
  const { type, payload, id } = event.data;

  await ensureInit();

  if (type === 'fibonacci') {
    const result = fibonacci(payload.n);
    self.postMessage({ id, result: result.toString() });
  }
};
// src/hooks/useWasmWorker.ts
import { useRef, useCallback, useEffect } from 'react';

export function useWasmWorker() {
  const workerRef = useRef<Worker | null>(null);
  const pendingRef = useRef<Map<string, (result: unknown) => void>>(new Map());

  useEffect(() => {
    const worker = new Worker(
      new URL('../workers/math.worker.ts', import.meta.url),
      { type: 'module' }
    );

    worker.onmessage = (event) => {
      const { id, result } = event.data;
      const resolve = pendingRef.current.get(id);
      if (resolve) {
        resolve(result);
        pendingRef.current.delete(id);
      }
    };

    workerRef.current = worker;
    return () => worker.terminate();
  }, []);

  const calculate = useCallback(
    (type: string, payload: unknown): Promise<unknown> => {
      return new Promise((resolve) => {
        const id = crypto.randomUUID();
        pendingRef.current.set(id, resolve);
        workerRef.current?.postMessage({ type, payload, id });
      });
    },
    []
  );

  return { calculate };
}

Настройка Vite для работы с Wasm

Vite требует отдельного плагина для корректной работы с .wasm-файлами:

npm install -D vite-plugin-wasm vite-plugin-top-level-await
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';

export default defineConfig({
  plugins: [
    react(),
    wasm(),
    topLevelAwait(),
  ],
  optimizeDeps: {
    exclude: ['wasm-math'],
  },
});

Плагин wasm позволяет импортировать .wasm-файлы напрямую через import, а topLevelAwait — использовать await на верхнем уровне модуля.

Типичные ошибки и их решение

MIME-тип для .wasm

Сервер должен отдавать .wasm-файлы с типом application/wasm. В dev-режиме Vite делает это автоматически, но на продакшн-сервере (nginx, Apache) может потребоваться явная настройка:

types {
  application/wasm wasm;
}

Маршаллинг данных

Wasm работает только с числовыми типами и памятью. Передача строк и объектов требует сериализации. wasm-bindgen скрывает это за обёртками, но при прямой работе с инстансом нужно вручную записывать данные в линейную память:

// Запись строки в память Wasm-инстанса вручную
function writeString(instance: WebAssembly.Instance, str: string): number {
  const memory = instance.exports.memory as WebAssembly.Memory;
  const encoder = new TextEncoder();
  const bytes = encoder.encode(str);
  const ptr = (instance.exports.alloc as Function)(bytes.length);
  const view = new Uint8Array(memory.buffer, ptr, bytes.length);
  view.set(bytes);
  return ptr;
}

Повторная инициализация

Wasm-модуль должен инициализироваться один раз. Защита через useRef в хуке выше (initialized.current) предотвращает двойной вызов в StrictMode React.

Производительность: замеры

Перед оптимизацией стоит убедиться, что выгода реальна. Сравните реализации через performance.now():

function benchmarkFibonacci(n: number, iterations = 100) {
  // JavaScript
  const jsStart = performance.now();
  for (let i = 0; i < iterations; i++) {
    fibJS(n);
  }
  const jsTime = performance.now() - jsStart;

  // WebAssembly
  const wasmStart = performance.now();
  for (let i = 0; i < iterations; i++) {
    fibonacci(n);
  }
  const wasmTime = performance.now() - wasmStart;

  return { jsTime, wasmTime, speedup: jsTime / wasmTime };
}

Для простых функций Wasm может не давать выигрыша из-за накладных расходов на вызов. Реальная разница заметна на задачах от нескольких миллисекунд.

Загрузка только при необходимости

Если Wasm нужен не при старте приложения, используйте динамический импорт:

import { lazy, Suspense } from 'react';

const HeavyProcessor = lazy(() => import('./components/HeavyProcessor'));

export function App() {
  const [showProcessor, setShowProcessor] = useState(false);

  return (
    <div>
      <button onClick={() => setShowProcessor(true)}>
        Открыть процессор
      </button>
      {showProcessor && (
        <Suspense fallback={<p>Загрузка...</p>}>
          <HeavyProcessor />
        </Suspense>
      )}
    </div>
  );
}

Внутри HeavyProcessor инициализация Wasm произойдёт только когда компонент реально понадобится.

Итог

Интеграция WebAssembly в React-приложение сводится к нескольким шагам: скомпилировать модуль (через wasm-pack для Rust или Emscripten для C/C++), корректно настроить сборщик, написать хук для управления жизненным циклом загрузки и использовать типизированные массивы при передаче данных. Для задач, которые действительно требуют скорости нативного кода, этот путь оправдан и даёт измеримый результат.

Для более глубокого погружения в React и современные паттерны разработки смотрите курс на PurpleSchool.

Стрелочка влевоКак работает Virtual DOM в ReactReact PWA: Progressive Web App с ReactСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

React тестирование с Vitest
React с TanStack Router
Zustand: продвинутые паттерны, persist middleware и slicesUncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовАрхитектура больших React приложений: Feature-Sliced DesignuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыДокументирование компонентов в React: Storybook, JSDoc и READMEКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactTailwind CSS с ReactТестирование хуковSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookReact Toastify - уведомления в ReactВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
Valtio: атомарный state management в ReactuseState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьЧто такое React.js и как его использоватьКак работает JSX связка React и HTMLКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий