Присвячені, спільні та робочі процеси сервісу: вибір правильної нитки браузера
Зрозумійте, у чому полягають відмінності між пристроями 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 у власному окремому контексті, щоб основна нитка залишалася вільною для роботи з інтерфейсом.
Як сторінка та робітник співпрацюють
Browser
│
┌─────────┴─────────┐
│ │
↓ ↓
Main Thread Worker Thread
│ │
UI / DOM Heavy Work
Events Calculations
Rendering Parsing
Interaction Processing
│ │
└──── Messages ─────┘
Сторінка надсилає дані до робітника, викликаючи метод postMessage у об’єкті робітника:
worker.postMessage(data);
Усередині робітника глобальний метод postMessage надсилає результат назад на сторінку:
postMessage(result);
Обидві сторони не мають спільних звичайних змінних; все передається у вигляді повідомлень.
Чому робітники не можуть впливати на 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
Робітник прив’язаний до свого створювача та ніколи не ділиться з несумісними сторінками; два вкладки мають по два незалежні робітники.
Хороші кандидати на спеціалізованого робітника
Вони ефективні для обробки завдань, що вимагають багато ресурсів CPU, у межах одного додатку. Типовим прикладом є перетворення великого об’єму даних 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
Підходять також шифрування, стиснення, обробка великих файлів та складні обчислення. Правило: якщо завдання, що вимагають багато ресурсів CPU, спричиняють проблеми з інтерфейсом, варто розглянути можливість використання спеціалізованого робітника.
Спільні працівники для кількох вкладок
Тепер уявімо, що та сама програма відкрита одночасно у кількох середовищах браузера:
Tab A
Tab B
Tab C
Спільний працівник дозволяє скриптам у кількох вікнах, вкладках чи іфреймах під’єднуватися до одного працівника, за умови, що вони мають спільне походження:
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
Тому вони відіграють ключову роль у прогресивних веб-додатках (PWAs) та дизайнах, орієнтованих на роботу без інтернету.
Порівняння трьох типів поруч
Спеціалізований працівник
Коротко кажучи, це „фонова нитка, яка належить одній сторінці“:
Page
│
↓
Dedicated Worker
Він підходить для обчислень, що вимагають потужності CPU, парсингу, обробки зображень та обробки даних.
Спільний працівник
Коротко кажучи, це „один працівник, яким можуть користуватися кілька сторінок з одного походження“:
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 Workers є трьома конкретними рішеннями на три окремі запитання.
Пов’язана література
- Увімкнення підтримки роботи офлайн у веб-додатках за допомогою Service Workers — Дізнайтеся, як використовувати Service Workers та API Cache для миттєвого завантаження веб-сайту та його подальшої роботи навіть без підключення до Інтернету.
- Як браузер малює елементи та яке місце займає React — Дізнайтеся, як критичний шлях відображення, процес узгодження даних, технологія Fiber та планувальник спільно перетворюють оновлення React на пікселі на екрані.