Що таке web workers?
Web Workers дозволяють запускати JavaScript у фонових потоках, ізольованих від головного, щоб важкі обчислення не заморожували UI.
Теорія
TL;DR
- Головний потік як кухар, що готує страви по одній. Web Workers - це помічники у підсобці, які передають готовий результат через записки
- Немає доступу до DOM, немає спільної пам'яті, тільки передача повідомлень через
postMessage/onmessage - Дані копіюються глибоко через алгоритм structured clone - не передаються за посиланням
- Правило: якщо обчислення блокує UI більше ніж на 50мс, виноси у Worker
- Не варто: для DOM-оновлень, швидких задач до 10мс або даних які не клонуються
Швидкий приклад
// main.js
const worker = new Worker('worker.js');
worker.postMessage(21); // надсилаємо вхідне значення
worker.onmessage = (e) => console.log(e.data); // виводить: 42
// worker.js
self.onmessage = (e) => {
const result = e.data * 2; // важкі обчислення не заморозять UI
self.postMessage(result); // відповідь до головного потоку
};Worker запускає worker.js в окремому потоці. postMessage надсилає дані, onmessage їх отримує з іншого боку. Головний потік весь цей час залишається вільним.
Модель ізоляції
Web Worker запускає повністю ізольований JavaScript-контекст зі своїм глобальним об'єктом (self), без доступу до window, document або DOM. Коли викликаєш postMessage, браузер використовує алгоритм structured clone, щоб глибоко скопіювати дані перед передачею між потоками. Обидва потоки завжди отримують власну копію. Це усуває помилки зі спільним станом, які роблять багатопотоковий код в інших мовах таким складним.
Головне тут одне: між головним потоком і Worker за замовчуванням немає спільної пам'яті. Ти передаєш дані, а не посилання.
Коли використовувати
Використовуй Web Worker коли:
- Цикл або обчислення займає більше 50мс і сторінка підгальмовує
- Обробляєш пікселі canvas для великих зображень (4K і вище)
- Парсиш JSON або CSV файли розміром більше 1МБ
- Потрібні криптографічні операції або ML-інференс у браузері
Не варто коли:
- Задача потребує читання або запису DOM
- Операція завершується менш ніж за 10мс
- Дані, які треба передати, не клонуються (функції, DOM-вузли, об'єкти з методами прототипу)
Як браузер створює воркери
Chrome (V8) і Firefox (SpiderMonkey) створюють реальний потік ОС через примітиви на зразок pthreads. Рушій JS парсить і компілює скрипт воркера в цьому потоці повністю ізольовано. Повідомлення накопичуються в окремому каналі на кожен Worker. Крок structured clone блокує відправника на час серіалізації, тому відправка 100МБ ArrayBuffer синхронно - дорога операція. Для великих бінарних даних є SharedArrayBuffer (вимагає заголовків COOP/COEP), або можна передати право власності через postMessage(buffer, [buffer]) без копіювання.
Типові помилки
Спроба звернутися до DOM:
// worker.js - одразу викине помилку
document.getElementById('log').innerText = 'done'; // ReferenceError: document is not definedУ Worker немає document. Надішли повідомлення до головного потоку і нехай він оновить DOM.
Передача даних, що не клонуються:
// main.js - викине помилку
worker.postMessage({ fn: () => console.log('hi') }); // DataCloneErrorФункції, DOM-вузли та об'єкти з методами прототипу не клонуються. Передавай прості значення, об'єкти, масиви, Blob або ArrayBuffer.
Припущення про спільні змінні:
let count = 0; // main.js
worker.postMessage(count++); // worker бачить 0, main збільшує до 1 - це окремі контекстиІзольовані контексти означають відсутність спільних глобальних змінних. Завжди передавай весь стан який потрібен через повідомлення.
Забуваєш викликати terminate:
Worker займає пам'ять поки не викликати terminate(). Це та помилка яку я бачу найчастіше на code review: React-компонент створює Worker всередині useEffect, але не повертає нічого з того ефекту, і Worker живе вічно. Виклик worker.terminate() в cleanup - обов'язковий. Пул з 4 воркерів є стандартною практикою; створення нового на кожну задачу додає 50-200мс накладних витрат.
Великі повідомлення:
Клонування 100МБ ArrayBuffer блокує обидва потоки на 500мс і більше. Розбивай дані на частини або використовуй transferable objects: worker.postMessage(buffer, [buffer]) передає право власності замість копіювання.
Де зустрічається
- Figma запускає розрахунки макету для режиму прототипування у Workers, щоб canvas тримав 60fps
- Fabric.js використовує Workers для парсингу SVG в редакторах canvas
- Three.js виносить компіляцію WebGL шейдерів з головного потоку, щоб уникнути просадок fps
- Photopea обробляє фільтри 4K зображень у Workers через OffscreenCanvas
- Comlink (використовується у Vercel) обгортає Workers у proxy-об'єкти, щоб вони виглядали як звичайні async-функції
Питання на співбесіді
Q: Як передавати дані між головним потоком і Worker?
A: postMessage надсилає дані, onmessage отримує їх з іншого боку. Дані глибоко копіюються через алгоритм structured clone, тому зміни на одному боці не впливають на інший.
Q: Які типи даних можна передати через postMessage?
A: Прості значення, звичайні об'єкти, масиви, Blob, ArrayBuffer та ImageData. Функції, DOM-вузли та об'єкти з Symbol-ключами викидають DataCloneError.
Q: Яка різниця між Worker і SharedWorker?
A: SharedWorker дозволяє кільком вкладкам або скриптам підключатися до одного і того ж екземпляра воркера за ім'ям, ділячи один фоновий потік. Звичайний Worker належить одному скрипту.
Q: Як дебажити Web Workers?
A: У Chrome DevTools відкрий Sources і знайди іконку Worker у лівій бічній панелі. Кожен Worker має власну область з точками зупинки та окремою консоллю.
Q: (Senior) Чому реалізовують пул воркерів замість створення нового на кожну задачу?
A: Створення Worker займає 50-200мс. Пул з 4-8 воркерів амортизує цю вартість, повторно використовуючи потоки. Задачі стають у чергу і розподіляються до наступного вільного Worker через round-robin. Неактивні Workers завершуються через 30 секунд, щоб звільнити пам'ять.
Приклади
Базовий: подвоїти число у фоні
// worker.js
self.onmessage = (e) => {
const result = e.data * 2;
self.postMessage(result);
};
// main.js
const worker = new Worker('worker.js');
worker.postMessage(21);
worker.onmessage = (e) => console.log(e.data); // 42
worker.terminate(); // звільняємо ресурсиНайпростіший можливий Worker: отримати число, повернути число. Головний потік не блокується поки відбувається обчислення.
Проміжний: фільтр grayscale з OffscreenCanvas
// ImageFilterWorker.js
self.onmessage = async (e) => {
const { imageData } = e.data;
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
const out = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i = 0; i < out.data.length; i += 4) {
// формула яскравості
const gray = 0.3 * out.data[i] + 0.59 * out.data[i + 1] + 0.11 * out.data[i + 2];
out.data[i] = out.data[i + 1] = out.data[i + 2] = gray;
}
ctx.putImageData(out, 0, 0);
self.postMessage(await canvas.convertToBlob());
};
// React-компонент
const worker = new Worker('ImageFilterWorker.js');
worker.onmessage = (e) => setFilteredImage(URL.createObjectURL(e.data));
worker.postMessage({ imageData: ctx.getImageData(0, 0, width, height) });Обробка пікселів 4K зображення в головному потоці заморозила б UI на кілька секунд. У Worker компонент залишається інтерактивним весь цей час. OffscreenCanvas дає Worker повний canvas API без будь-якої залежності від DOM.
Просунутий: обробка помилок і завершення
// main.js
const worker = new Worker('error-worker.js');
worker.onerror = (e) => {
console.error('Worker впав:', e.message); // "Intentional crash"
worker.terminate();
};
worker.postMessage('crash');
// error-worker.js
self.onmessage = (e) => {
if (e.data === 'crash') throw new Error('Intentional crash');
self.postMessage('ok');
};Необроблені помилки у Worker запускають onerror в головному потоці. Сам головний потік при цьому не падає. Але якщо onerror не приєднано, помилка просто зникає. Завжди вішай його. terminate() вбиває Worker миттєво без жодного callback - зберігай потрібний стан до виклику.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.