Адаптаваныя, спільныя і працоўнікі служб: выбір правага потока прыгледвача
З'ясавайце, які разніцы є межах сферы дзеяння, способу передачы інформаціі та меты між працоўнікамі, якія займаюцца рэалізацыёй, спільнай працёю та адказам дзеўэпта, і дазнайцеся, калі кожны з іх насправдзе павышае якасць веб-зявітку.
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 workers — это тры конкрэтныя адпаведзі на тры разлічныя запытанні.
Спаднёе чытанне
- Уваўлеччы падтрымку роботы без інтэрнету ў веб-дзеянах з аднойчынай службой — Дазведацеся, як выкарыстоўваць Service Workers і Cache API, каб веб-сайт завантажваўся мгновенна і продаваў работу нават без з’яўлення інтэрнет-з’язку.
- Як браузер адрасавае элементы і якая роля у гэтым React — Дазведацеся, як Critical Rendering Path, процес супраўляння дадзеннямі, Fiber і Scheduler разам працуюць, каб апдэйты React ператварыліся на пікселі на экране.