Реалізація 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 прямо зараз.







