Web Workers — вычисления, которые не морозят интерфейс
Одна и та же тяжёлая задача: в главном потоке она замораживает страницу, а в Web Worker считается в фоне — спиннер и FPS не сбиваются. Потрогай разницу вживую.
JS в браузере однопоточный: пока считается тяжёлый цикл, страница не рисуется и не реагирует на клики. Хотел показать это не словами, а телом — чтобы фриз можно было почувствовать пальцем, и рядом же увидеть, как воркер его убирает.
Воркер — это настоящий второй поток ОС, а не «асинхронность». Он реально считает параллельно, но не имеет доступа к DOM и общается только через postMessage (структурное клонирование копирует данные). Для больших массивов есть transferable-буферы — передаются без копии, за наносекунды.
Первый вариант создавал воркер из Blob-URL — так меньше файлов. Но строгий CSP сайта (default-src self, без blob:) его резал. Пришлось отдать воркер статическим файлом /workers/heavy-task.js и добавить в CSP worker-src self — заодно честнее: same-origin вместо inline-кода.
Почему интерфейс вообще замерзает
JavaScript в браузере крутится в одном потоке. Тот же поток считает ваш код, раскладывает лейаут, рисует кадр и обрабатывает клики. Пока он занят синхронным циклом на пару секунд — страница мертва: анимации стоят, кнопки не нажимаются, курсор в текстовом поле не мигает.
Ниже — живой индикатор на requestAnimationFrame (спиннер, FPS и «пульс»). Нажми «На главном потоке» — та же rAF-петля живёт на занятом потоке, поэтому всё замрёт на время счёта. Потом нажми «В Web Worker» — и почувствуй разницу.
Как это устроено
new Worker('/workers/heavy-task.js') поднимает отдельный поток. Общение — только сообщениями:
// главный поток
worker.postMessage(40_000_000);
worker.onmessage = (e) => console.log(e.data.ms);
// worker.js — свой глобальный контекст, без window и без DOM
self.onmessage = (e) => {
let acc = 0;
for (let i = 0; i < e.data; i++) acc += Math.sqrt(i) * Math.sin(i);
self.postMessage({ acc });
};Тяжёлый расчёт в демо (√·sin в цикле на 40 млн итераций) абсолютно одинаков в обоих случаях — меняется только где он исполняется. В главном потоке заминка UI ≈ времени задачи; в воркере — стабильные ~16 мс на кадр.
Что дальше умеют воркеры
Это самый наглядный сценарий, но не единственный:
- Фоновые запросы и парсинг — тянуть и разбирать большой JSON, не подвешивая рендер.
- SharedWorker — один поток на несколько вкладок: общий сокет, синхронизация состояния между окнами одного сайта.
- OffscreenCanvas + transferable — фильтровать/декодировать картинки прямо в воркере, отдавая пиксели буфером без копии.
Каждый из них — тема для отдельной заметки; здесь важно было показать корень: тяжёлое считаем не там, где рисуем.