JavaScript Web Workers
Изучите Web Workers в JavaScript: запускайте код в фоновом потоке, сохраняйте отзывчивость UI, общайтесь через postMessage и эффективно передавайте данные.
JavaScript выполняет ваш код в единственном главном потоке — том же потоке, который располагает элементы страницы, отрисовывает пиксели и обрабатывает клики и нажатия клавиш. Этот единственный поток является сердцем цикла событий: он берёт одну задачу, выполняет её до конца, затем переходит к следующей. Поэтому когда функция делает что-то по-настоящему тяжёлое — обрабатывает большой array, разбирает файл размером в несколько мегабайт, тысячи раз хеширует пароль — цикл оказывается заблокирован внутри этой функции. Ничто другое произойти не может: прокрутка подтормаживает, кнопки перестают реагировать, страница выглядит замёрзшей, пока работа не завершится.
Web Workers решают эту проблему, запуская скрипт в отдельном фоновом потоке параллельно с главным потоком. Тяжёлая работа уходит с критического пути, UI остаётся отзывчивым, а два потока общаются между собой, передавая сообщения.
Почему таймеров недостаточно
Первый инстинкт — обернуть медленную работу в setTimeout и надеяться, что она выполнится «в фоне». Но это не так. Таймеры лишь откладывают задачу на следующий виток того же цикла — когда коллбэк наконец срабатывает, он по-прежнему выполняется в главном потоке и по-прежнему блокирует всё во время выполнения. (Смотрите статью о планировании с setTimeout и setInterval — там объясняется, как эта очередь работает на самом деле.)
// This still freezes the page — it just freezes it 50ms later.
setTimeout(() => {
let total = 0;
for (let i = 0; i < 5_000_000_000; i++) total += i;
console.log(total);
}, 50);Web Worker — это другое: его код выполняется в совершенно другом потоке, поэтому главный поток свободен продолжать отрисовку и реагировать на события, пока worker трудится.
Создание Worker
Worker — это отдельный JavaScript-файл. Вы создаёте его, передав конструктору Worker URL этого файла:
const worker = new Worker('worker.js');Браузер запускает новый поток, загружает worker.js и начинает его выполнять. С этого момента два скрипта общаются только через сообщения — они не разделяют никаких переменных, функций и object.
Вот минимальная пара файлов.
main.js
const worker = new Worker('worker.js');
worker.postMessage('Hello from the main thread');
worker.onmessage = (event) => {
console.log('Main received:', event.data);
};worker.js
self.onmessage = (event) => {
console.log('Worker received:', event.data);
self.postMessage('Hello back from the worker');
};Обмен сообщениями через postMessage
Коммуникация двунаправленная и асинхронная. Главный поток вызывает worker.postMessage(data) и слушает через worker.onmessage; внутри worker self.postMessage(data) отправляет ответ, а self.onmessage получает входящие. Каждый обработчик получает MessageEvent, а полезная нагрузка находится в свойстве .data.
Передаваемые данные копируются, а не передаются по ссылке, с помощью алгоритма structured clone. Это означает, что можно передавать строки, числа, boolean, array, простые object, Map, Set, Date, ArrayBuffer и многое другое — но не функции, DOM-узлы и экземпляры классов с методами. Поскольку это копия, изменение object на одной стороне никогда не влияет на другую.
Вот полный цикл «запрос — ответ». Worker вычисляет n-е число Фибоначчи с помощью наивного рекурсивного алгоритма — намеренно медленного, именно того рода работы, которая вызвала бы подтормаживание UI, если бы выполнялась в главном потоке.
main.js
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
console.log(`fib(${event.data.n}) = ${event.data.result}`);
};
// The page stays interactive while this runs on the worker thread.
worker.postMessage({ n: 42 });worker.js
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
self.onmessage = (event) => {
const { n } = event.data;
const result = fib(n);
self.postMessage({ n, result });
};Главный поток отправляет запрос и немедленно возвращается к обработке кликов и отрисовке. Когда worker завершит работу, результат придёт в виде сообщения — без заморозки, без подтормаживания спиннера.
Глобальная область видимости Worker
Внутри worker нет window. Глобальным object является self (экземпляр DedicatedWorkerGlobalScope), и принципиально важно, что там нет document и нет DOM. Worker не может читать или изменять страницу; если ему нужно обновить UI, он отправляет сообщение и позволяет главному потоку сделать это.
Код внутри Web Worker не может обращаться к DOM. Там нет document, нет window и нет доступа к элементам страницы. Всё визуальное должно передаваться обратно в главный поток через postMessage. Именно это ограничение делает workers безопасными для параллельного выполнения — нет разделяемого состояния UI, которое можно было бы испортить.
Тем не менее workers далеко не пусты. Область видимости worker предоставляет множество полезных API:
importScripts('a.js', 'b.js')для синхронной загрузки классических скриптов.fetchиXMLHttpRequestдля сетевых запросов.- Таймеры:
setTimeout,setInterval. console,crypto,TextEncoder/TextDecoder,WebSocket, IndexedDB и многое другое.
Это делает workers отличным местом для сетевых запросов, разбора данных, сжатия и шифрования — работ, которые самодостаточны и производят результат, который можно передать обратно на страницу.
Обработка ошибок и завершение
Если worker выбрасывает необработанную ошибку, она появляется в главном потоке через worker.onerror:
worker.onerror = (event) => {
console.error(`Worker error: ${event.message} (${event.filename}:${event.lineno})`);
};Worker продолжает работать до тех пор, пока его не остановят. Вы можете остановить его снаружи с помощью worker.terminate(), что немедленно завершает поток — любая незавершённая работа теряется:
worker.terminate();Или worker может завершить себя изнутри, когда он закончил:
// inside worker.js
self.close();Завершение простаивающих workers освобождает память; долго живущие workers, обрабатывающие много сообщений, вполне можно оставлять активными.
Transferable Objects: перемещение вместо копирования
Structured clone удобен, но копирует данные. Для большой бинарной нагрузки — скажем, буфера изображения размером 50 МБ — копирование и тратит память, и отнимает время. Transferable objects позволяют передавать право собственности вместо копирования: данные перемещаются в другой поток без каких-либо копий, и отправитель теряет к ним доступ.
Для этого передайте второй аргумент в postMessage — список объектов для передачи:
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB
// Transfer ownership of the buffer to the worker (no copy).
worker.postMessage({ buffer }, [buffer]);
console.log(buffer.byteLength); // 0 — this thread can no longer use itПосле передачи buffer.byteLength на отправляющей стороне равен 0: память теперь принадлежит worker. Передача идеальна для ArrayBuffer и типизированных arrays, построенных на их основе — смотрите статью об ArrayBuffer и бинарных arrays, где описана структура этих бинарных данных. Другие transferable-объекты: MessagePort, ImageBitmap и OffscreenCanvas.
Содержимое второго аргумента должно также присутствовать в сообщении. В worker.postMessage({ buffer }, [buffer]) буфер упоминается в полезной нагрузке и перечислен как transferable. Если указать что-то, недостижимое из сообщения, браузер выбросит DataCloneError.
Module Workers
По умолчанию worker является классическим скриптом и использует importScripts() вместо синтаксиса ES-модулей. Передайте { type: 'module' } — и worker станет модульным, способным использовать статический и динамический import:
const worker = new Worker('worker.js', { type: 'module' });worker.js
import { compress } from './compression.js';
self.onmessage = (event) => {
self.postMessage(compress(event.data));
};Module workers — это современный стандарт для нового кода: они дают правильный импорт, строгий режим и более чистый граф зависимостей.
Другие виды Workers
Простой new Worker(...) создаёт dedicated worker: он принадлежит единственной странице, которая его создала. Есть два связанных — но отличных — типа workers, которые нужно уметь различать:
SharedWorker— единственный экземпляр worker, разделяемый несколькими вкладками, окнами или iframe одного origin. Страницы подключаются к нему черезMessagePort, что делает его полезным для координации состояния или единственного сетевого соединения между вкладками. Это не более быстрый dedicated worker — это разделяемый worker.- Service Worker — специальный worker, выступающий сетевым прокси между страницей и сетью, обеспечивающий кеширование, офлайн-поддержку и push-уведомления. Он основан на событиях и существует дольше жизни страницы. Это совсем другая задача, нежели у dedicated worker («выполни вычисление вне основного потока»); подробнее об этом читайте в статье о Service Workers.
Практическое правило: используйте dedicated Web Worker для переноса CPU-интенсивной работы с главного потока, SharedWorker — для совместного использования одного worker несколькими вкладками одного сайта, и Service Worker — для управления сетевыми запросами и создания офлайн-приложений.
Когда использовать Web Workers
Web Workers оправдывают себя, когда задача ограничена CPU и длится достаточно долго, чтобы ощущаться как подтормаживание:
- Тяжёлые вычисления — физика, анализ данных, большие сортировки и агрегации.
- Обработка изображений и видео, включая манипуляции с пикселями вне экрана.
- Разбор и сжатие больших файлов (CSV, JSON, архивы).
- Криптография — хеширование и шифрование без заморозки ввода.
- Обработка больших наборов данных перед передачей компактного результата на отрисовку.
Если узкое место — ожидание сети, а не вычисления, worker обычно не нужен — fetch уже асинхронен и не блокирует поток. Workers блистают тогда, когда именно CPU не даёт главному потоку работать.