Реализация Web Workers для фоновых вычислений на сайте
Представьте: пользователь загружает CSV-файл на 10 МБ, и интерфейс зависает на 5 секунд. Скролл не реагирует, кнопки не нажимаются. Это происходит, потому что JavaScript — однопоточный, и парсинг блокирует UI. Решение — выгрузка тяжёлых вычислений в Web Workers. Наш опыт: 10+ лет в высоконагруженных веб-приложениях, 50+ проектов с Workers, ускорение взаимодействия в 3–5 раз. Обращайтесь — мы внедрим многопоточность под ваш стек и сроки.
Web Workers не имеют доступа к DOM, window, document. Общение с main thread — только через сообщения (postMessage/onmessage). Это ограничение — защита от race conditions. Мы используем типизированные обёртки и Transferable Objects для эффективной передачи больших данных. Подробнее об API — в документации MDN.
Какие задачи решают Web Workers?
- Парсинг и трансформация больших CSV/JSON (>1 MB) — ускорение в 4 раза за счёт выгрузки из основного потока.
- Шифрование/дешифрование данных (AES, RSA) — занимает 500 ms в main thread, в Worker — без влияния на UI.
- Рендеринг Canvas-графики через OffscreenCanvas — все рисование в фоне, основной поток только реагирует на ввод пользователя.
- Алгоритмы поиска и сортировки на больших массивах (10⁶+ элементов) — с пулом Worker-ов параллельно за секунды.
- Компрессия данных (pako, zlib) — сжатие перед отправкой на сервер без блокировки интерфейса.
- Физические симуляции и ray tracing — для интерактивных визуализаций.
Как внедрить Web Workers без потери производительности?
Ключевые принципы:
- Transferable Objects — передача ArrayBuffer, ImageBitmap, OffscreenCanvas по ссылке (без копирования) для больших данных.
- Пул Worker-ов — по числу ядер CPU, распределение задач через единую очередь.
- Типизированные обёртки — исключение ошибок типов при обмене сообщениями.
- Обработка ошибок на уровне планировщика — перезапуск упавшего Worker-а без потери данных.
| Метод передачи данных | Скорость | Поддержка | Применение |
|---|---|---|---|
| Structured clone | 1x (копирование) | Все браузеры | Мелкие данные (<1MB) |
| Transferable Objects | 10x+ (без копирования) | Chrome, Firefox, Safari 16.4+ | ArrayBuffer, ImageBitmap |
Подробнее о Transferable Objects
Передача объектов по ссылке возможна для ArrayBuffer, ImageBitmap, OffscreenCanvas, ReadableStream, WritableStream. После вызова postMessage с массивом transferables исходный объект становится недоступен в отправителе, что исключает гонки данных. Это критично для буферов от 10 MB — задержка снижается в десятки раз.Базовая структура Worker
// worker.ts — фоновый поток self.onmessage = ({ data }: MessageEvent) => { const result = heavyComputation(data.payload); self.postMessage({ type: 'RESULT', payload: result }); }; // main.ts — основной поток const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); worker.postMessage({ type: 'PROCESS', payload: largeArray }); worker.onmessage = (event) => console.log('Результат:', event.data.payload); worker.onerror = (error) => console.error('Worker error:', error.message); worker.terminate(); Типизированная обёртка
Работать с сырым postMessage неудобно. Типизированная обёртка решает это:
type WorkerMessage<T extends Record<string, unknown>> = { [K in keyof T]: { type: K; payload: T[K] } }[keyof T]; interface WorkerRequest { SORT: { array: number[]; direction: 'asc' | 'desc' }; FILTER: { data: Record<string, unknown>[]; query: string }; PARSE_CSV: { content: string }; } interface WorkerResponse { SORT_DONE: number[]; FILTER_DONE: Record<string, unknown>[]; PARSE_CSV_DONE: Record<string, string>[]; ERROR: { message: string }; } class TypedWorker { private worker: Worker; private pending = new Map<string, { resolve: Function; reject: Function }>(); private seq = 0; constructor(workerUrl: URL) { this.worker = new Worker(workerUrl, { type: 'module' }); this.worker.onmessage = ({ data }) => { const { id, type, payload } = data; const handler = this.pending.get(id); if (!handler) return; this.pending.delete(id); if (type === 'ERROR') handler.reject(new Error(payload.message)); else handler.resolve(payload); }; } send<K extends keyof WorkerRequest>(type: K, payload: WorkerRequest[K]): Promise<WorkerResponse[`${K}_DONE` & keyof WorkerResponse]> { return new Promise((resolve, reject) => { const id = String(++this.seq); this.pending.set(id, { resolve, reject }); this.worker.postMessage({ id, type, payload }); }); } terminate() { this.worker.terminate(); } } Передача больших данных: Transferable Objects
postMessage копирует данные по умолчанию. Для больших ArrayBuffer это дорого. Transferable Objects передаются по ссылке (owner transfer), без копирования:
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10 MB const view = new Float32Array(buffer); // Заполняем данными... // Передаём без копирования — после этого buffer в main thread недоступен worker.postMessage({ type: 'PROCESS', payload: buffer }, [buffer]); // В Worker self.onmessage = (event: MessageEvent) => { const buf = event.data.payload as ArrayBuffer; const view = new Float32Array(buf); // обрабатываем... ctx.postMessage({ type: 'DONE', payload: buf }, [buf]); }; Transferable: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream, WritableStream.
OffscreenCanvas — рендеринг в Worker
// main.ts const canvas = document.getElementById('chart') as HTMLCanvasElement; const offscreen = canvas.transferControlToOffscreen(); worker.postMessage({ type: 'INIT_CANVAS', canvas: offscreen }, [offscreen]); worker.postMessage({ type: 'RENDER', data: chartData }); // chart-worker.ts let ctx: OffscreenCanvasRenderingContext2D; self.onmessage = (event: MessageEvent) => { const { type, canvas, data } = event.data; if (type === 'INIT_CANVAS') { ctx = canvas.getContext('2d')!; return; } if (type === 'RENDER') renderChart(ctx, data); }; Пул Worker-ов для параллельной обработки
class WorkerPool { private workers: Worker[] = []; private queue: Array<{ resolve: Function; reject: Function; message: unknown }> = []; private idle: Worker[] = []; constructor(workerUrl: URL, poolSize = navigator.hardwareConcurrency || 4) { for (let i = 0; i < poolSize; i++) { const worker = new Worker(workerUrl, { type: 'module' }); worker.onmessage = (event) => this.onWorkerMessage(worker, event); worker.onerror = (error) => this.onWorkerError(worker, error); this.workers.push(worker); this.idle.push(worker); } } execute(message: unknown): Promise<unknown> { return new Promise((resolve, reject) => { const task = { resolve, reject, message }; const worker = this.idle.pop(); if (worker) this.dispatch(worker, task); else this.queue.push(task); }); } private dispatch(worker: Worker, task: { resolve: Function; reject: Function; message: unknown }) { (worker as any).__resolve = task.resolve; (worker as any).__reject = task.reject; worker.postMessage(task.message); } private onWorkerMessage(worker: Worker, event: MessageEvent) { (worker as any).__resolve?.(event.data); this.scheduleNext(worker); } private onWorkerError(worker: Worker, error: ErrorEvent) { (worker as any).__reject?.(new Error(error.message)); this.scheduleNext(worker); } private scheduleNext(worker: Worker) { const next = this.queue.shift(); if (next) this.dispatch(worker, next); else this.idle.push(worker); } terminate() { this.workers.forEach((w) => w.terminate()); } } Как обеспечить отказоустойчивость при многопоточности?
Падение Worker-а не должно влиять на основное приложение. Для этого планировщик перезапускает упавший Worker и перераспределяет задачи. В пуле используем паттерн heartbeat: каждый Worker отправляет сигнал раз в 10 секунд. Если сигнал отсутствует 20 секунд — Worker считается упавшим и заменяется новым. Это даёт uptime 99,9% при штатных нагрузках.
| Параметр | Одиночный Worker | Пул Worker-ов |
|---|---|---|
| Время обработки 100 задач по 100ms | ~10 сек (последовательно) | ~1.2 сек (8 Workers) |
| Отказоустойчивость | Низкая (падение = потеря) | Высокая (перезапуск) |
| Использование памяти | Минимальное (1 поток) | Умеренное (по числу потоков) |
Что входит в работу
- Анализ узких мест в текущем приложении (профилирование, Core Web Vitals).
- Проектирование архитектуры: выбор количества Worker-ов, схемы передачи данных, стратегии fallback для старых браузеров.
- Реализация типизированных обёрток и пула (или интеграция с React/Vue через кастомные хуки).
- Покрытие unit-тестами (мокирование Worker-ов изолированно).
- Документация и обучение команды.
- Гарантия поддержки и доработок в течение месяца после сдачи.
Этапы работы
- Аудит — профилирование текущей производительности, выявление кандидатов для выноса.
- Проектирование — выбор паттерна (один Worker, пул, OffscreenCanvas), определение контрактов сообщений.
- Реализация — написание Worker-ов, интеграция, типизация.
- Тестирование — юнит-тесты, нагрузочное тестирование (имитация 20+ одновременных задач).
- Деплой и мониторинг — внедрение, отслеживание LCP/INP после релиза.
Почему выбирают нас?
Мы — команда инженеров с 10+ лет опыта в веб-разработке. Реализовали 50+ проектов с Web Workers — от парсинга логов на клиенте до real-time дашбордов с OffscreenCanvas. Наши решения ускоряют вычисления в 3–5 раз без потери стабильности. Свяжитесь с нами — мы оценим ваш проект и предложим сроки от 1 до 5 дней в зависимости от сложности. Закажите внедрение Web Workers прямо сейчас.







