Галоўная / Артыкулы / Адаптаваныя, спільныя і працоўнікі служб: выбір правага потока прыгледвача

Адаптаваныя, спільныя і працоўнікі служб: выбір правага потока прыгледвача

З'ясавайце, які разніцы є межах сферы дзеяння, способу передачы інформаціі та меты між працоўнікамі, якія займаюцца рэалізацыёй, спільнай працёю та адказам дзеўэпта, і дазнайцеся, калі кожны з іх насправдзе павышае якасць веб-зявітку.

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 у сваёй самастойнай сэрэге, тады галоўны поток застаецца вольным для работы з інтэрфейсам.

Як сторанка і рабочы процес саавершаюць задачі

Уявіце два потаки, якія з’ѐеднаны каналам для перадачы паведамленняў: галоўны поток керуе 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

Глобальны простор імен рабочага процесу не мае доступу да гэтых элементаў:

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

Паколькі ён перехопляе запыткі, ён стварае адлеглыя можлівасці, кэшаванне, персональную обработку запыткаў, пуш-паведамленні і сінхронізацыю у фоне.

Распавешчаны межа стораніцай і серверам

Спадзяймося, ўжотык відвідае ваш сайт:

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

Ён падходзіць для прыкладнаў, якія працуюць без падключэння да інтарнету, кэшавання, перехоплення запытаў, пуш-паўтарэнняў і фонавай сінхронізацыі.

Адна-лінейны ўзвод кожнага

Адна лінія для кожнага:

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 — это тры конкрэтныя адпаведзі на тры разлічныя запытанні.

    Спаднёе чытанне