Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Web Workers в React: как вынести тяжелые вычисления в фоновый поток и избавиться от фризов интерфейса

Тимофей Ищенко

Коротко: Разбираем архитектуру Web Workers в React и TypeScript: интеграция в Vite и Next.js, кастомный хук useWorker, Transferable Objects и оптимизация INP.

JavaScript в браузере по умолчанию выполняется в одном основном потоке (main thread). В этом же потоке браузер рассчитывает стили, строит layout, выполняет перерисовку (paint) и обрабатывает пользовательский ввод: клики, скролл, ввод текста в инпуты.

Когда React-приложение сталкивается с CPU-bound задачей — например, клиентской фильтрацией массива на 200 000 строк, расчетом криптографических хэшей или парсингом массивных JSON-структур, — Event Loop застревает на выполнении скрипта. Для пользователя это выглядит как подвисание интерфейса: анимации замирают, курсор в текстовом поле не мигает, а метрика Interaction to Next Paint (INP) деградирует до критических значений.

Асинхронность через Promise, async/await или setTimeout здесь не помогает: они решают проблемы I/O-bound операций (ожидание ответа сети или таймера), но сам код коллбэка все равно выполняется в основном потоке. Единственный нативный способ перенести реальные вычислительные нагрузки с основного потока — использовать Web Workers API.


Архитектура Web Workers API: что происходит под капотом

Web Worker запускает JavaScript-код в полностью изолированном системном потоке, работающем параллельно главному контексту приложения.

+-------------------------------------------------------------+
|                     Main Thread (UI)                        |
|   React Components  <--->  DOM / Virtual DOM  <---> Events  |
+-------------------------------------------------------------+
             |                                    ^
             | postMessage(data)                  | onmessage(event)
             v                                    |
+-------------------------------------------------------------+
|                     Background Thread                       |
|                       Web Worker                            |
|        (CPU-bound: фильтрация, парсинг, криптография)       |
+-------------------------------------------------------------+

Изоляция потока и доступные API

Рабочий поток воркера изолирован от среды страницы:

  • Нет доступа к DOM: внутри воркера нельзя обращаться к объектам document, window, элементам интерфейса и React Context.
  • Нет доступа к Web Storage: localStorage и sessionStorage недоступны.
  • Доступные API: воркер поддерживает сетевые запросы (fetch, XMLHttpRequest), таймеры (setTimeout, setInterval), работу с хранилищем IndexedDB, криптографический модуль crypto.subtle, а также графический контекст OffscreenCanvas.

Structured Clone против Transferable Objects

По умолчанию обмен данными между основным потоком и воркером происходит через postMessage с использованием алгоритма Structured Clone. Браузер сериализует объект в основном потоке, копирует его в памяти и десериализует внутри воркера.

Для небольших объектов накладные расходы незаметны. Однако при передаче структур размером в десятки мегабайт сериализация сама по себе может заблокировать main thread на десятки миллисекунд.

Чтобы избежать копирования, используют Transferable Objects (например, ArrayBuffer, MessagePort, ImageBitmap). В этом случае происходит передача владения памятью по ссылке с нулевым копированием (zero-copy). Как только буфер передан воркеру, он мгновенно очищается в исходном потоке (byteLength становится равен 0), что исключает состояние гонки (race conditions).

// Передача ArrayBuffer через Transferable Objects без копирования
const buffer = new ArrayBuffer(1024 * 1024 * 32); // 32 MB
worker.postMessage({ type: 'PROCESS_BUFFER', buffer }, [buffer]);

console.log(buffer.byteLength); // 0 (память перешла воркеру)

Dedicated Workers, Shared Workers и Module Workers

  • Dedicated Worker: экземпляр привязан к конкретному контексту страницы (вкладке). Завершает работу при закрытии вкладки или явном вызове worker.terminate().
  • Shared Worker: может обслуживать несколько вкладок, фреймов или окон одного origin.
  • Module Worker: воркер, инициализированный с флагом { type: 'module' }. Позволяет использовать внутри стандартные ES-модули (import/export), разделять код и подключать внешние npm-библиотеки без устаревшего метода importScripts().

Настройка и интеграция с React: Vite, Webpack 5 и Next.js

Современные инструменты сборки из коробки поддерживают нативный синтаксис Module Workers, стандартизированный спецификацией WHATWG.

Инициализация через URL-конструктор в Vite и Webpack 5

Чтобы сборщик корректно собрал файл воркера в отдельный чанк, создал правильный URL и применил TypeScript-транспиляцию, используется синтаксис new URL(..., import.meta.url):

// calculation.worker.ts
self.onmessage = (event: MessageEvent<number[]>) => {
  const result = event.data.reduce((acc, curr) => acc + curr, 0);
  self.postMessage(result);
};

export {};
// Инициализация внутри React-модуля
const worker = new Worker(
  new URL('./calculation.worker.ts', import.meta.url),
  { type: 'module' }
);

Строгая типизация сообщений в TypeScript

Отсутствие строгих типов в postMessage — частая причина багов в production. Для безопасного обмена сообщениями создается контракт через дискриминантные объединения (discriminated unions):

// worker.types.ts
export type WorkerRequest = 
  | { type: 'FILTER_DATA'; payload: { query: string; items: string[] } }
  | { type: 'PARSE_CSV'; payload: { rawData: string } };

export type WorkerResponse =
  | { type: 'FILTER_SUCCESS'; result: string[] }
  | { type: 'PARSE_SUCCESS'; result: Record<string, unknown>[] }
  | { type: 'ERROR'; error: string };

Поддержка SSR в Next.js

Класс Worker — это браузерный API. При выполнении кода на стороне сервера (Node.js/Edge в Next.js, Remix или Astro) прямое обращение к new Worker() вызовет ошибку ReferenceError: Worker is not defined.

Инициализацию воркера необходимо изолировать внутри useEffect или оборачивать в проверку доступности окружения:

if (typeof window !== 'undefined') {
  // Безопасная инициализация воркера на клиенте
}

Паттерн кастомного хука useWorker

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

Реализация универсального хука

// useWorker.ts
import { useEffect, useRef, useState, useCallback } from 'react';

interface WorkerState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

export function useWorker<TPayload, TResult>(
  workerFactory: () => Worker
) {
  const [state, setState] = useState<WorkerState<TResult>>({
    data: null,
    loading: false,
    error: null,
  });

  const workerRef = useRef<Worker | null>(null);
  const promiseResolversRef = useRef<{
    resolve: (value: TResult) => void;
    reject: (reason: Error) => void;
  } | null>(null);

  useEffect(() => {
    // Создаем экземпляр воркера при монтировании
    const worker = workerFactory();
    workerRef.current = worker;

    worker.onmessage = (event: MessageEvent<TResult>) => {
      setState({ data: event.data, loading: false, error: null });
      if (promiseResolversRef.current) {
        promiseResolversRef.current.resolve(event.data);
        promiseResolversRef.current = null;
      }
    };

    worker.onerror = (event: ErrorEvent) => {
      const error = new Error(event.message || 'Worker error occurred');
      setState({ data: null, loading: false, error });
      if (promiseResolversRef.current) {
        promiseResolversRef.current.reject(error);
        promiseResolversRef.current = null;
      }
    };

    // Очистка: уничтожаем поток при размонтировании компонента
    return () => {
      worker.terminate();
      workerRef.current = null;
    };
  }, []);

  const run = useCallback((payload: TPayload): Promise<TResult> => {
    if (!workerRef.current) {
      return Promise.reject(new Error('Worker is not initialized'));
    }

    setState((prev) => ({ ...prev, loading: true, error: null }));

    return new Promise((resolve, reject) => {
      promiseResolversRef.current = { resolve, reject };
      workerRef.current?.postMessage(payload);
    });
  }, []);

  return {
    run,
    data: state.data,
    loading: state.loading,
    error: state.error,
  };
}

Использование хука в React-компоненте

// HeavyDataGrid.tsx
import React, { useMemo } from 'react';
import { useWorker } from './useWorker';

interface FilterPayload {
  items: string[];
  query: string;
}

export const HeavyDataGrid: React.FC<{ dataset: string[] }> = ({ dataset }) => {
  // Фабрика создания воркера мемоизируется, чтобы избежать пересоздания на каждый рендер
  const createWorker = useMemo(
    () => () => new Worker(new URL('./filter.worker.ts', import.meta.url), { type: 'module' }),
    []
  );

  const { run, data: filteredData, loading, error } = useWorker<FilterPayload, string[]>(createWorker);

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    run({ items: dataset, query: e.target.value });
  };

  return (
    <div className="p-4">
      <input
        type="text"
        placeholder="Поиск по 500k записей..."
        onChange={handleSearch}
        className="border p-2 rounded"
      />
      
      {loading && <p>Фильтрация в фоновом потоке...</p>}
      {error && <p className="text-red-500">Ошибка: {error.message}</p>}

      <ul className="mt-4 max-h-96 overflow-y-auto">
        {(filteredData || dataset).slice(0, 100).map((item, idx) => (
          <li key={idx}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

Реальные сценарии применения в продакшене

1. Фильтрация и нечеткий поиск (Fuzzy Search) по большим наборам данных

Поиск по сложным структурам (100k+ записей) с регулярными выражениями или алгоритмами типа расстояния Левенштейна моментально блокирует рендеринг. Перенос алгоритма в воркер позволяет интерфейсу сохранять стабильные 60/120 FPS даже в момент активного набора символов в строке поиска.

2. Парсинг и валидация CSV/JSON/Excel на стороне клиента

При загрузке пользователем CSV-файла размером 50–100 МБ парсинг через библиотеки вроде PapaParse в основном потоке замораживает вкладку на несколько секунд. Воркер может принять File или ArrayBuffer, распарсить его, провалидировать через Zod или Yup и вернуть в React только готовый для отображения срез данных.

3. Обработка графики и рендеринг через OffscreenCanvas

Интерфейс OffscreenCanvas позволяет отрисовывать графику, манипулировать пикселями (фильтры, сжатие, масштабирование) и строить визуализации в воркере:

// Передача управления канвасом в воркер
const canvas = canvasRef.current;
if (canvas) {
  const offscreen = canvas.transferControlToOffscreen();
  worker.postMessage({ canvas: offscreen }, [offscreen]);
}

4. Клиентское шифрование и генерация хешей

Генерация ключей, расчет контрольных сумм файлов (SHA-256, MD5) перед их загрузкой на S3 или клиентское сквозное шифрование (E2EE) — классические CPU-bound задачи, требующие изоляции в фоновом потоке.


Когда Web Workers не нужны: ограничения и антипаттерны

Использование воркеров сопряжено с накладными расходами. Внедрять их повсеместно — ошибка.

Накладные расходы на запуск и сериализацию

Создание нового воркера требует выделения системных ресурсов и времени на инициализацию JS-контекста (обычно от 5 до 30 мс в зависимости от устройства). Для мелких задач (сортировка 100 элементов) время сериализации данных через postMessage превысит время самих вычислений.

Отличие Web Workers от React Concurrent Features (useTransition)

Многие разработчики путают назначение useTransition / useDeferredValue и Web Workers:

Характеристика React Concurrent Features (useTransition) Web Workers
Где выполняется код В основном потоке (Main Thread) В отдельном системном потоке (Background)
Принцип работы Прерывание и приоритизация рендера Virtual DOM Физически параллельное выполнение JS-кода
Подходит для Тяжелого рендера дерева компонентов Тяжелых CPU-вычислений (алгоритмы, математика, парсинг)
Влияние на UI Длинная синхронная задача в JS все равно вызовет лаг Не блокирует Event Loop основного потока

Антипаттерн: создание сотен инстансов без пула

Запуск воркера на каждый клик или рендер приводит к перерасходу оперативной памяти и падению вкладки (OOM). Если требуется выполнять множество параллельных задач, проектируют Worker Pool — пул из фиксированного числа воркеров (соответствующего navigator.hardwareConcurrency), распределяющий задачи по очереди.


Чек-лист: внедрение Web Workers в React-приложение

  1. Профиль задачи: убедитесь через Chrome DevTools Performance, что проблема вызвана именно Long Tasks (задачами длиннее 50 мс) в фазе скриптинга, а не избыточными рендерами компонентов.
  2. Изоляция логики: выделите «чистую» функцию вычислений без замыканий на состояние приложения и DOM API.
  3. Оптимизация передачи данных: для массивов байтов и бинарных структур используйте Transferable Objects, для остальных структур — минимально достаточный payload.
  4. Управление жизненным циклом: всегда вызывайте worker.terminate() внутри cleanup-функции useEffect при размонтировании компонента.
  5. Безопасность SSR: проверяйте наличие глобального объекта window перед инициализацией воркера.
  6. Обработка ошибок: добавляйте обработчик worker.onerror для перехвата исключений внутри изолированного контекста.

Часто задаваемые вопросы (FAQ)

Чем Web Worker отличается от useTransition в React 18/19?

useTransition лишь сообщает React, что обновление состояния имеет низкий приоритет и его рендеринг можно прервать, если пользователь выполнит новое действие. Однако код продолжает исполняться в основном потоке. Если внутри функции выполняется тяжелый расчет (например, цикл на миллион итераций), useTransition не спасет от блокировки Event Loop. Web Worker полностью выносит исполнение кода в отдельный системный поток.

Можно ли использовать Redux или Zustand внутри Web Worker?

Напрямую передать инстанс стора в воркер нельзя, так как объекты с функциями и методами не подлежат клонированию через Structured Clone. Однако можно запускать независимый стор внутри воркера и пересылать сериализованные экшены/состояния в основной поток через postMessage, либо использовать специализированные архитектурные надстройки.

Как отлаживать код внутри Web Worker в DevTools?

В Chrome DevTools воркеры отображаются на вкладке Sources в блоке Threads слева. При падении воркера или установке точки останова (debugger) отладчик переключает контекст на поток воркера. Вызовы console.log() из воркера также выводятся в общую консоль браузера с указанием имени файла воркера.

Что быстрее: Structured Clone или Transferable Objects?

Transferable Objects работают значительно быстрее на больших объемах данных (десятки и сотни мегабайт), так как они не копируют данные в памяти, а просто передают владение ссылкой на ArrayBuffer. Скорость операции близка к мгновенной (zero-copy overhead). Structured Clone копирует память, что создает дополнительную нагрузку на CPU и сборщик мусора (Garbage Collector).

Сколько Web Workers можно запускать одновременно?

Количество одновременно выполняемых потоков ограничено физическими ядрами процессора, о чем сообщает параметр navigator.hardwareConcurrency. Создание большего числа активных потоков приводит к переключению контекста (context switching) на уровне ОС и снижению общей производительности. Рекомендуется использовать пул из N - 1 воркеров, где N — число логических ядер.


Заключение

React отлично оптимизирует работу с пользовательским интерфейсом и виртуальным деревом, но он бессилен против архитектурных ограничений однопоточного JavaScript. Перенос ресурсоемких задач в Web Workers — стандартный инженерный подход для высоконагруженных клиентских приложений. Правильное разделение обязанностей (React управляет представлением, а фоновые потоки выполняют вычисления) обеспечивает стабильный отклик интерфейса, высокий показатель INP и плавный UX независимо от сложности выполняемых алгоритмов.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также