IS
Все эксперименты
живое

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-кода.

Стек
Web WorkersrequestAnimationFrameTypeScript

Почему интерфейс вообще замерзает

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 — фильтровать/декодировать картинки прямо в воркере, отдавая пиксели буфером без копии.

Каждый из них — тема для отдельной заметки; здесь важно было показать корень: тяжёлое считаем не там, где рисуем.