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), но интеграция требует понимания асинхронных потоков, борьбы с гонками и грамотного управления пулом. Подключите нас, разберём, где у вас есть тормоза в браузере и какую часть можно вытащить в фон без переписывания фронта целиком.