Головна / Статті / Присвячені, спільні та робочі процеси сервісу: вибір правильної нитки браузера

Присвячені, спільні та робочі процеси сервісу: вибір правильної нитки браузера

Зрозумійте, у чому полягають відмінності між пристроями dedicated, shared та service за обсягом функцій, способом передачі даних та метою, а також дізнайтеся, коли кожен з них насправді покращує веб-додаток.

2425 слів

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 worker: перехоплення мережевих даних, кешування та можливості роботи офлайн.
  • Перш ніж їх впровадити, оцініть обсяг завдань, які вони блокують, переконайтеся, що це переважає над додатковими витратами на обробку повідомлень, та перевірте підтримку браузером. У цьому світлі Service Workers є трьома конкретними рішеннями на три окремі запитання.

    Пов’язана література