Основной поток JavaScript — однопоточная среда, где долгие вычисления блокируют отрисовку интерфейса и обработку событий. Это становится заметно, когда приложение выполняет обработку изображений, сложную математику (например, трансформации graphs), или потоковую аналитику. Пользователь видит замирание интерфейса, скролл «тормозит», а анимации дропают кадры.
Web Workers предоставляют механизм запуска скриптов в фоновых потоках без доступа к DOM, но с возможностью асинхронного взаимодействия с основным потоком. Рассмотрим, как и когда их применять.
Проблема: блокировка основного потока
Предположим, мы разрабатываем инструмент для преобразования изображений. Этот код на vanilla JS блокирует интерфейс:
function applyFilter(imageData) {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
// Интенсивная обработка каждого пикселя
const avg = (pixels[i] + pixels[i+1] + pixels[i+2]) / 3;
pixels[i] = avg;
pixels[i+1] = avg;
pixels[i+2] = avg;
}
return imageData;
}
// В основном потоке:
canvasContext.putImageData(applyFilter(rawImageData), 0, 0);
При обработке изображения 4096x2160 цикл выполняется ~8.8 миллионов раз. Интерфейс не будет реагировать на действия пользователя 2-3 секунды.
Решение: вынос логики в Web Worker
- Базовая реализация
Создадим image-processor.worker.js
:
self.addEventListener('message', (e) => {
const { imageData } = e.data;
const processed = applyFilter(imageData); // Та же функция, что и раньше
self.postMessage({ processed });
});
В основном коде:
const worker = new Worker('image-processor.worker.js');
worker.postMessage({ imageData: rawImageData },
[rawImageData.data.buffer] // Используем Transferable Objects
);
worker.onmessage = (e) => {
canvasContext.putImageData(e.data.processed, 0, 0);
};
Transferable Objects исключают копирование данных между потоками, передавая владение ArrayBuffer. Это критично для больших данных.
- Оркестрация при масштабировании
Для распределения нагрузки между несколькими воркерами создадим пул:
class WorkerPool {
constructor(size, workerScript) {
this.workers = Array.from({ length: size }, () => new Worker(workerScript));
this.queue = [];
this.workers.forEach(worker => {
worker.onmessage = () => {
if (this.queue.length) {
const { task, resolve } = this.queue.shift();
worker.postMessage(task);
this.pending = resolve;
} else {
this.available.push(worker);
}
};
});
}
schedule(task) {
return new Promise(resolve => {
const worker = this.available.pop() || null;
if (worker) {
worker.postMessage(task);
this.pending = resolve;
} else {
this.queue.push({ task, resolve });
}
});
}
}
// Инициализация:
const pool = new WorkerPool(navigator.hardwareConcurrency || 4, 'worker.js');
Это решает проблемы с последовательным выполнением задач и учитывает количество CPU-ядер.
- Тонкости передачи данных
Web Workers не имеют доступа к:
- DOM
window
объекту- родительским переменным
Для связи используйте:
- Структурированные клоны — для простых объектов (медленно для больших данных).
- Transferable Objects — как в примере выше.
- SharedArrayBuffer — когда нужно разделяемое состояние между потоками (требует настроек CORS и COOP/COEP).
Когда не использовать Web Workers
- Короткие операции (<50 мс). Накладные расходы на создание воркера и передачу данных перевесят выгоду.
- Задачи, требующие доступа к DOM. Например, измерение layout’а элемента.
- Синхронные API (
localStorage
,alert
).
Реальные кейсы
Case 1: Data Grid с live-фильтрацией
При фильтрации 100k строк через сложные предикаты переложите вычисления в воркер. Это позволит продолжить рендерить анимации сортировки.
Case 2: Анализ производительности
Сэмплирование и агрегация метрик (FPS, время отклика) в фоне без влияния на измеряемые показатели.
Case 3: Декодирование видео через WebCodecs
Обработка кадров в отдельном потоке сохранит плавность интерфейса плеера.
Производительность: цифры
На M1 MacBook Pro обработка изображения 4K:
- Основной поток: 2.3s, 120 FPS -> 12 FPS во время обработки.
- Web Worker: 2.5s (с учетом передачи данных), FPS стабилен.
Пул из 4 воркеров сокращает время для пакетной обработки 10 изображений с 25s до 7s.
Интеграция с React
Используйте хук для управления состоянием воркера:
function useImageProcessor() {
const [result, setResult] = useState();
const workerRef = useRef();
useEffect(() => {
workerRef.current = new Worker('worker.js');
workerRef.current.onmessage = (e) => setResult(e.data);
return () => workerRef.current.terminate();
}, []);
const process = (data) => workerRef.current.postMessage(data);
return { process, result };
}
Но учитывайте, что повторные рендеры не должны создавать новых воркеров. Используйте ленивую инициализацию или статический пул.
Выводы
Web Workers — не серебряная пуля, но мощный инструмент для оптимизации. Они требуют:
- Явного разделения кода на модули.
- Понимания стоимости передачи данных.
- Аккуратной работы с состоянием.
Параллелизм в JavaScript остаётся сложной темой, но современные API и паттерны (WebAssembly, OffscreenCanvas) расширяют возможности. Начните с профилирования: Chrome DevTools > Performance покажет, где основной поток блокируется дольше допустимого. Если длительная задача не требует DOM — это кандидат на вынос в Web Worker.