Рабочие процессы, предназначенные для отдельных задач, общие и сервисные: выбор подходящей нити браузера
Поймите, в чём различия между рабочими процессами типа dedicated, shared и service с точки зрения объёма задач, способа передачи сообщений и цели их использования, а также узнайте, когда каждый из них действительно способствует улучшению веб-приложения.
JavaScript на странице выполняется в одном основном потоке, тем не менее современные приложения остаются отзывчивыми при обработке больших файлов, работают офлайн и принимают уведомления во время бездействия. Значительная часть этого достигается благодаря воркерам, которые запускают скрипты вне основного потока. Однако термин «воркер» охватывает три разных инструмента, и выбор неподходящего из них приводит к пустой трате усилий. В этом руководстве объясняется, для чего предназначен каждый тип, как они взаимодействуют и когда их использование нецелесообразно.
В эту группу входят три компонента:
Web Workers
│
├── Dedicated Worker
│
├── Shared Worker
│
└── Service Worker
Почему основному потоку нужна помощь
По умолчанию ваш код делит один поток с обновлением DOM, обработкой ввода, отрисовкой и анимацией:
JavaScript
↓
DOM updates
↓
User interactions
↓
Rendering
↓
Animations
Поскольку эти задачи выполняются по очереди, длительный синхронный вызов, такой как тот, что показан ниже, блокирует поток до его завершения:
const result = expensiveCalculation();
Пока он не закончится, браузер не может обрабатывать ввод или отрисовывать кадры. Пользователь это ощущает следующим образом:
User clicks button
↓
Heavy JavaScript starts
↓
Main thread is busy
↓
UI becomes sluggish
↓
Calculation finishes
↓
UI becomes responsive again
Рабочий процесс решает эту проблему путем выполнения JavaScript в собственном отдельном контексте, так что основной поток остается свободным для работы с интерфейсом.
Как взаимодействуют страница и рабочий процесс
Представьте два потока, соединенных каналом сообщений: основной поток отвечает за UI, DOM, события и отрисовку, а рабочий процесс занимается вычислениями, парсингом и обработкой данных:
Browser
│
┌─────────┴─────────┐
│ │
↓ ↓
Main Thread Worker Thread
│ │
UI / DOM Heavy Work
Events Calculations
Rendering Parsing
Interaction Processing
│ │
└──── Messages ─────┘
Страница отправляет данные в рабочий процесс, вызывая метод postMessage у объекта рабочего процесса:
worker.postMessage(data);
Внутри рабочего процесса глобальный метод postMessage отправляет результат обратно на страницу:
postMessage(result);
Обе стороны не имеют общих переменных; все данные передаются в виде сообщений.
Почему рабочие процессы не могут влиять на DOM
Глобальный область видимости рабочего процесса не имеет доступа к элементам DOM:
document
window
DOM elements
Такая строка внутри файла рабочего процесса просто не сработает, потому что там не определен document:
// worker.js
document.querySelector("#app");
Правильный подход заключается в выполнении вычислений в рабочем процессе, отправке результата и последующем применении его к странице основным потоком:
Main Thread
│
│ postMessage(data)
↓
Worker
│
│ performs calculation
↓
Worker
│
│ postMessage(result)
↓
Main Thread
│
↓
Update DOM
Сохранение контроля над DOM в одном потоке означает, что фоновый код никогда не сможет изменить интерфейс во время отрисовки.
Три типа рабочих процессов в одном взгляде
У них разный диапазон доступа: один создатель, несколько контекстов одного происхождения или сетевой слой:
┌──────────────────────────────┐
│ Web Workers │
├──────────────────────────────┤
│ │
│ Dedicated Worker │
│ → One script/client │
│ │
│ Shared Worker │
│ → Multiple same-origin │
│ browsing contexts │
│ │
│ Service Worker │
│ → Network / caching / │
│ offline / background │
│ capabilities │
│ │
└──────────────────────────────┘
Специализированные рабочие процессы для тяжелых вычислений
Специализированный рабочий процесс принадлежит скрипту, который его создал. Когда команда говорит «перенести эти вычисления в рабочий процесс», имеется в виду именно этот тип.
Со стороны страницы вы создаете рабочий процесс по URL скрипта, отправляете ему число и записываете полученный результат:
const worker = new Worker("worker.js");
worker.postMessage(1000000);
worker.onmessage = (event) => {
console.log("Result:", event.data);
};
Рабочий процесс прослушивает сообщения, суммирует все целые числа, меньшие полученного значения, и отвечает с общей суммой:
// worker.js
self.onmessage = (event) => {
const number = event.data;
let result = 0;
for (let i = 0; i < number; i++) {
result += i;
}
self.postMessage(result);
};
Цикл отправки и получения данных прост:
Main Thread
│
│ 1000000
↓
Dedicated Worker
│
│ Calculate
↓
Result
│
↓
Main Thread
Рабочий процесс связан со своим создателем и никогда не делится с несвязанными страницами; два вкладки получают по два независимых рабочих процесса.
Хорошие кандидаты на использование отдельного рабочего процесса
Они полезны для задач, требующих больших вычислительных ресурсов в рамках одного приложения. Типичным примером является обработка большого объема данных от API: рабочий процесс преобразует данные, а основной поток лишь отображает их.
API Response
↓
Large JSON
↓
Worker
↓
Parse / Transform
↓
Main Thread
↓
Render UI
Изменение размера изображений и их фильтрация выполняются по аналогичной схеме:
Image
↓
Worker
↓
Resize / Transform
↓
Result
↓
UI
То же самое касается фильтрации, сортировки и агрегации больших наборов данных:
Large Dataset
↓
Worker
↓
Filtering
Sorting
Aggregation
↓
UI
Подходят также шифрование, сжатие, обработка больших файлов и сложные вычисления. Правило: если задачи, требующие больших вычислительных ресурсов, приводят к замедлению работы интерфейса, стоит рассмотреть использование отдельного рабочего процесса.
Общий рабочий процесс для нескольких вкладок
Теперь предположим, что одно и то же приложение открыто одновременно в нескольких браузерных контекстах:
Tab A
Tab B
Tab C
Общий рабочий процесс позволяет скриптам в нескольких окнах, вкладках или iframe подключаться к одному рабочему процессу, при условии, что у них совпадает исходный адрес:
Shared Worker
/ | \
/ | \
Tab A Tab B Tab C
Без него каждая вкладка создает свою собственную копию рабочего процесса:
Tab A → Worker A
Tab B → Worker B
Tab C → Worker C
С его использованием каждая вкладка подключается к одному экземпляру рабочего процесса:
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Обмен сообщениями через порты
До общего рабочего процесса можно добраться через явно указанный MessagePort. Страница запускает порт и отправляет сообщение:
const worker = new SharedWorker("worker.js");
worker.port.start();
worker.port.postMessage("Hello");
Каждый новый клиент вызывает событие connect в рабочем процессе, обработчик которого прослушивает порт этого клиента:
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (event) => {
console.log(event.data);
};
};
Основное практическое отличие от специализированного рабочего процесса заключается в порту: при большом количестве клиентов каждое соединение требует собственного канала, а ответы возвращаются по порту соответствующей вкладки.
Сценарий с несколькими вкладками
Представьте внутренний инструмент, где разные вкладки отображают разные виды данных:
Tab 1 → Dashboard
Tab 2 → Reports
Tab 3 → Analytics
Один фоновый компонент может хранить общее состояние или координировать коммуникацию между всеми вкладками:
Shared Worker
│
┌──────────┼──────────┐
↓ ↓ ↓
Tab 1 Tab 2 Tab 3
│ │ │
└──────────┼──────────┘
↓
Shared State
Это позволяет избежать использования отдельного рабочего процесса для каждой вкладки, но все контексты должны иметь общее происхождение. Поддержка совместных рабочих процессов также исторически отставала по сравнению со специализированными рабочими процессами, особенно в некоторых мобильных браузерах, поэтому сначала проверьте текущие данные о совместимости.
Service workers для работы в сети и офлайн
Service worker — это самый недооцененный тип. Это не переименованный специализированный рабочий процесс; он находится между вашим приложением, браузером и сетью:
Browser
│
↓
Service Worker
│
├── Cache
│
├── Network
│
├── Offline response
│
└── Background capabilities
Поскольку он перехватывает запросы, он обеспечивает работу при отсутствии подключения к сети, кэширование, персонализированную обработку запросов, уведомления типа push и синхронизацию в фоновом режиме.
Расположение между страницей и сервером
Предположим, пользователь посещает ваш сайт:
https://example.com
Без сервисного рабочего процесса каждый запрос направляется непосредственно из браузера на сервер:
Browser
↓
Internet
↓
Server
При наличии зарегистрированного сервисного рабочего процесса запросы сначала проходят через него, и он может обслужить их из кэша или переслать в сеть:
Browser
↓
Service Worker
↓
├── Cache
│
└── Network
Он определяет судьбу каждого запроса. Стратегия с использованием кэша сначала позволяет мгновенно получить данные, а в остальных случаях выполняет запрос и кэширует результаты при необходимости:
Request
↓
Is it cached?
│
├── YES → Return cached response
│
└── NO
↓
Network
↓
Response
↓
Cache if appropriate
Приложения, работающие в режиме офлайн, создаются на основе этой логики. Чтобы ознакомиться с пошаговым руководством, посмотрите инструкцию по включению поддержки офлайн-режима в веб-приложениях с использованием сервисных рабочих процессов.
Регистрация и жизненный цикл
Браузер управляет жизненным циклом, который здесь представлен упрощённо:
Register
↓
Download
↓
Install
↓
Activate
↓
Control Pages
Сначала происходит регистрация скрипта при наличии API:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
Затем браузер обрабатывает установку, активацию и обновления. В отличие от этого, специальный рабочий процесс создаётся непосредственно при вызове конструктора, что никогда не применимо к сервисным рабочим процессам:
new Worker(...)
Они требуют безопасного контекста (HTTPS, при этом в режиме разработки допускается localhost). Новый рабочий процесс не может управлять уже открытыми страницами до перезагрузки, если только он сам их не займёт, что часто создаёт путаницу при тестировании.
Перехват запросов
Минимальный сервисный рабочий процесс прослушивает события fetch; этот пример лишь записывает каждый URL:
self.addEventListener("fetch", (event) => {
console.log("Request:", event.request.url);
});
Настоящая кэширование основывается на этом механизме. Для файла app.js: если он находится в кэше — подавать его оттуда, в противном случае скачивать, сохранять и возвращать:
User requests app.js
↓
Service Worker
↓
Is app.js cached?
/ \
YES NO
↓ ↓
Return Network
Cache ↓
Cache
↓
Return
Поэтому они играют ключевую роль в прогрессивных веб-приложениях (PWA) и дизайнах, ориентированных на работу в автономном режиме.
Сравнение трех типов работников
Специализированный работник
Кратко говоря, это «фоновая нить, принадлежащая одной странице»:
Page
│
↓
Dedicated Worker
Он подходит для вычислений, требующих больших ресурсов процессора, парсинга, обработки изображений и обработки данных.
Общий работник
Кратко говоря, это «один работник, который могут использовать несколько страниц с одним источником»:
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Он подходит для фоновых задач, общих между разными контекстами браузера, а также для общей коммуникации или синхронизации состояния.
Рабочий процесс сервиса
Кратко говоря, это «слой между приложением и сетью»:
App
↓
Service Worker
↓
Cache / Network
Он подходит для приложений, работающих в автономном режиме, кэширования, перехвата запросов, уведомлений типа push и фоновой синхронизации.
Однострочное резюме каждого типа
Кратко о каждом:
Dedicated Worker
↓
"Do this heavy computation for me."
Shared Worker
↓
"Let multiple pages use this worker."
Service Worker
↓
"Help my web application interact with
the network and browser capabilities."
В таком формате эти три варианта сложно спутать.
Область видимости, обмен сообщениями и типичное использование
Дедикуированный: один скрипт, фоновые вычисления, postMessage(), отсутствие DOM:
Scope:
One script
Main purpose:
Background computation
Communication:
postMessage()
DOM access:
❌ No
Common use:
Heavy computation
Общий: несколько контекстов одного происхождения, MessagePort, отсутствие DOM, координация между вкладками:
Scope:
Multiple same-origin contexts
Main purpose:
Shared background work
Communication:
MessagePort
DOM access:
❌ No
Common use:
Cross-tab/shared worker communication
Служба: страницы в пределах её происхождения и области пути, события и API платформы, отсутствие DOM, кэширование, работа в офлайн-режиме, функции push и синхронизация:
Scope:
Origin/path controlled pages
Main purpose:
Network + background capabilities
Communication:
Events / messaging / APIs
DOM access:
❌ No
Common use:
Caching, offline, push, background sync
Когда воркеры замедляют работу
Воркеры не обязательно делают работу быстрее. Начальная настройка требует ресурсов, а данные должны передаваться между контекстами:
Main Thread
↕
Worker
Обмен сообщениями также занимает время. Для небольшой задачи эти расходы могут превзойти саму работу:
Small Task
↓
Worker overhead
↓
Communication
↓
Actual calculation
В таком случае рабочий процесс может работать медленнее, чем встроенный код. Используйте его только тогда, когда задача настолько объемная, что ее перенос на отдельный процесс приносит заметную выгоду.
Что на самом деле передается через границу
Сообщения копируются с использованием алгоритма структурированной копирования, поэтому большие объемы данных увеличивают время копирования. Объекты типа ArrayBuffer передаются без копирования, но отправитель теряет к ним доступ. SharedArrayBuffer позволяет использовать реальную общую память только при наличии дополнительных мер безопасности (изоляция между источниками). Для большинства приложений лучше использовать явные сообщения:
Main Thread
↓
postMessage()
↓
Worker
↓
postMessage()
↓
Main Thread
Использование отдельного рабочего процесса в React
Если приложение React обрабатывает большой набор данных во время отрисовки или в обработчике, интерфейс зависает:
React UI
↓
Large calculation
↓
Main thread blocked
↓
UI becomes sluggish
С использованием отдельного рабочего процесса компонент отправляет данные и обновляет состояние по мере получения результата:
React UI
│
├──────────────> Worker
│ │
│ ↓
│ Calculation
│ │
│ ↓
│<──────────── Result
│
↓
Update UI
React продолжает отрисовку во время вычислений на воркере. Создайте воркер с помощью эффекта и вызовите terminate() в его функции очистки, чтобы он не существовал дольше компонента. Это подходит для визуализации данных, редакторов изображений, обработки аудио и видео, больших файлов и аналитики на стороне клиента.
Выбор подходящего воркера
Задачи, требующие интенсивной работы CPU, предполагают использование специализированного воркера:
Do you have CPU-heavy JavaScript?
│
YES
↓
Use Dedicated Worker
Если требование звучит так:
Multiple tabs/pages need
the same worker
тогда тип, который необходимо оценить, — это:
Shared Worker
А если требование включает что-либо из следующего:
Caching
Offline support
Network interception
Push notifications
Background sync
тогда ответ — это:
Service Worker
Распространенные ошибки
Перенос всего в воркер
Используйте воркеры только там, где они решают конкретную проблему.
Ожидание доступа к DOM
Это не сработает:
worker.document.querySelector(...);
Рабочие процессы отправляют данные обратно; основной поток обновляет DOM.
Использование сервисного рабочего процесса для вычислений
Сервисные рабочие процессы влияют на сетевую активность и жизненный цикл приложения, причем браузер может остановить их во время бездействия. Для простых числовых вычислений, таких как:
1 million records
↓
complex calculation
Начните с отдельного рабочего процесса.
Игнорирование затрат на обмен сообщениями
Каждый обмен данных сопряжен с дополнительными расходами:
Main Thread
↕
Messaging
↕
Worker
Переносите только ту работу, которая достаточно объемна для выполнения в рамках одного обмена.
Заключение
Основной принцип: ресурсоемкие операции на JavaScript не должны без оснований блокировать основной поток. Эти три типа соответствуют трём проблемам:
Web Workers
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Dedicated Shared Service
Worker Worker Worker
│ │ │
One script Multiple Network /
uses it contexts offline
- Отдельный рабочий процесс: сложные вычисления для одной страницы.
- Общий рабочий процесс: фоновая работа, доступная нескольким контекстам с одинаковым исходным адресом.
Прежде чем начать использование Service Worker, измерьте время выполнения задачи, которая вызывает блокировку, убедитесь, что оно превышает затраты на обработку сообщений, и проверьте поддержку браузером. С такой точки зрения Service Worker представляют собой три конкретных решения для трех разных проблем.
Связанные статьи
- Включение поддержки работы в офлайн-режиме в веб-приложениях с помощью Service Workers — Узнайте, как использовать Service Worker и API Cache для мгновенной загрузки веб-сайта и его дальнейшей работы даже без подключения к Интернету.
- Как браузер отрисовывает содержимое и какова роль React — Узнайте, как критический путь отрисовки, процесс согласования данных, технология Fiber и планировщик взаимодействуют друг с другом, преобразуя обновления React в пиксели на экране.