Web Workers для тяжёлых вычислений в браузере — когда и как

Парсинг 50 МБ CSV, кадрирование 200 фотографий, генерация PDF, сжатие, шифрование — всё это блокирует UI, если делать в основном потоке. Web Workers вытаскивают вычисления в фон, страница не подвисает. Разбираем, когда воркеры реально помогают, а когда — overkill, и как их подружить с React/Vue.

Пользователь загружает CSV на 50 МБ, чтобы импортировать заказы. Браузер замер на 8 секунд: вкладка не отвечает, прокрутка стоит, кнопка "Отмена" не нажимается. После — заказы импортировались, но впечатление испорчено. И таких сценариев в админках полно.

Корень — JavaScript однопоточный. Любое тяжёлое вычисление в основном потоке блокирует UI: ни клики, ни анимации, ни ввод. Решение известно с 2009 — Web Workers. Воркер — отдельный фоновый поток без доступа к DOM, общается с основным через сообщения. Главный поток рисует, воркер считает.

Когда воркер реально нужен

Грубое правило — если операция занимает больше 50 мс на медленном устройстве, имеет смысл вытащить её в воркер. Конкретные кейсы из практики:

  • Парсинг больших файлов. CSV/JSON/XML на 10+ МБ, импорт прайса, выгрузки 1С. Особенно если PapaParse или fast-xml-parser.
  • Обработка изображений. Сжатие, ресайз, наложение водяного знака перед загрузкой. Библиотеки browser-image-compression и Pica умеют работать в воркере.
  • Генерация PDF/Excel в браузере. jsPDF, ExcelJS на больших таблицах — секунды зависания.
  • Криптография. Шифрование, подпись, проверка хешей. Web Crypto API доступен в воркерах.
  • Поиск и фильтрация по большим коллекциям. 10 000+ записей с нечётким поиском, lunr.js, fuzzy-search.
  • Парсинг и форматирование. Markdown-to-HTML, синтаксический подсветка кода (highlight.js).
  • WebAssembly-модули. Если запускаете wasm для перекодировки видео, ML-инференса — почти всегда в воркер.

Если операция меньше 16 мс (один кадр при 60 fps) — воркер не нужен. Накладные расходы на сериализацию сообщения съедят выигрыш.

Когда воркер не нужен

  • DOM-манипуляции. Воркер не видит DOM. Если задача — перерисовать список, помогут virtual scrolling и батчинг, а не воркер.
  • Сетевые запросы. fetch в основном потоке не блокирует UI — он асинхронный. Выносить в воркер бессмысленно.
  • Простая бизнес-логика. Сложить корзину, проверить форму, отрисовать график — это микросекунды, воркер только усложнит код.
  • Анимации. requestAnimationFrame + CSS transforms — этого хватит. Воркер для анимации — только если расчёты сложные (например, физика частиц).

Как создать воркер — три способа

1. Классический Web Worker. Отдельный JS-файл, в нём свой scope, общение через postMessage.

// main.js
const worker = new Worker('parser.worker.js');
worker.postMessage({ csv: largeCsvString });
worker.onmessage = (e) => {
  console.log('Parsed:', e.data.rows.length);
};

// parser.worker.js
import Papa from 'papaparse';
self.onmessage = (e) => {
  const result = Papa.parse(e.data.csv, { header: true });
  self.postMessage({ rows: result.data });
};

Просто, но писать вручную сериализацию и протокол — занудно.

2. Comlink. Библиотека от Google, оборачивает воркер в обычный async-объект. Под капотом — RPC через postMessage, но синтаксически как будто вызываешь функцию.

// worker.js
import * as Comlink from 'comlink';
const api = {
  parseCsv(csv) { return Papa.parse(csv, { header: true }).data; }
};
Comlink.expose(api);

// main.js
import * as Comlink from 'comlink';
const api = Comlink.wrap(new Worker('worker.js'));
const rows = await api.parseCsv(largeCsv);

Это наш дефолт — код в воркере читается как обычный модуль, не надо городить switch по типам сообщений.

3. Inline-worker через Blob. Когда лень делать отдельный файл — оборачиваем функцию в Blob:

const code = `self.onmessage = (e) => { /* ... */ }`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

Удобно для одноразовой задачи, но без бандлера импорты не работают.

Подводные камни

Сериализация дорогая. postMessage клонирует данные (structured clone). Для 100 МБ массива это секунды и удвоение памяти. Решение — Transferable Objects: ArrayBuffer, MessagePort, ImageBitmap передаются по ссылке, в исходном потоке становятся недоступны.

const buffer = new ArrayBuffer(1024 * 1024 * 100);
worker.postMessage(buffer, [buffer]); // transfer
// buffer.byteLength === 0 в main после этого

SharedArrayBuffer ограничен. Для настоящей общей памяти между потоками — SharedArrayBuffer. Но он требует заголовков Cross-Origin-Opener-Policy: same-origin и Cross-Origin-Embedder-Policy: require-corp. На многих сайтах их не выставить без ломки сторонних виджетов.

Воркер съедает RAM. Каждый воркер — отдельный V8 isolate, 5-15 МБ оверхеда. Плодить по воркеру на задачу — плохо. Используйте пул воркеров (workerpool, либо свой пул на 2-4 экземпляра).

Стектрейсы и отладка. Воркер в DevTools — отдельная вкладка Sources. Ошибки летят через onerror, не в обычный консольный handler.

Module workers. Современный синтаксис: new Worker('w.js', { type: 'module' }) — позволяет import. Поддержка во всех движках с 2023, но Vite/webpack умеют делать это автоматически: new Worker(new URL('./w.js', import.meta.url), { type: 'module' }).

React/Vue + воркеры

В React воркер живёт в useEffect, передача данных через useState. Готовые хуки:

  • react-use — useWorker для inline-функций;
  • @koale/useworker — обёртка над Comlink, но не обновлялась с 2022;
  • vite-plugin-comlink — самый удобный путь в Vite-проекте, воркеры импортируются как модули.

В Vue — то же самое плюс composable-обёртка от VueUse: useWebWorker и useWebWorkerFn.

Главное правило интеграции — воркер не должен знать про фреймворк. Чистая функция вход → выход. Это упрощает тестирование (jest-environment-node) и позволяет переиспользовать воркер в любом UI.

Замер: до и после

Внутренний кейс — парсинг прайса на 80 МБ XML в админке клиента. До: вкладка зависала на 12 секунд, мобильный Chrome падал на iPhone 12. После переноса XML-парсинга в воркер: основной поток свободен, прогресс-бар обновляется по postMessage каждые 5 %, на айфоне процесс занимает 18 секунд (дольше из-за слабого CPU), но UI отзывчив. Жалоб не было.

Замеряли через Performance API + requestIdleCallback. Total Blocking Time (один из CWV) упал с 9500 мс до 230 мс — главный показатель того, что воркер сработал.

Когда стоит подключать студию

Если в проекте есть админка с импортом/экспортом больших данных, генерация документов на лету, обработка фото перед загрузкой — почти наверняка воркеры улучшат UX. Сами по себе они бесплатные (доступны в любом браузере с 2012), но интеграция требует понимания асинхронных потоков, борьбы с гонками и грамотного управления пулом. Подключите нас, разберём, где у вас есть тормоза в браузере и какую часть можно вытащить в фон без переписывания фронта целиком.

Узнайте подробнее о наших компетенциях
Разработка, ИИ, автоматизация — что мы делаем и как.