Что такое Web worker?
СкопированоWeb worker — это API, которое позволяет выполнять код вне основного потока. Благодаря этому долгие или сложные вычисления, которые выполняются на воркерах, не блокируют пользовательский интерфейс (UI).
Веб-воркер создаётся в основном потоке. При создании воркеру передаётся URL-адрес скрипта. После загрузки создаётся отдельный поток, в котором выполнится скрипт воркера.
У скрипта будет свой собственный контекст, отличный от контекста окна window
. В основном потоке глобальный контекст привязывается к переменной window
, а в воркере — к переменной self
. Контекст выполнения веб-воркера WorkerGlobalScope отличается от контекста выполнения основного потока. У него нет доступа к объекту документа document
и DOM API
.
Особенности потока выполнения
СкопированоПоток, в котором выполняется код воркера, изолирован от основного. В Chromium каждому из этих потоков соответствует свой собственный экземпляр движка JavaScript. Из-за этого создание нового воркера считается «тяжёлой» операцией. Обычно предполагают, что воркеров будет немного, и они будут долго жить.
Потоки могут общаться между собой через отправку сообщений. Используйте для отправки сообщений функцию post
.
Как создать и запустить?
СкопированоВсё просто: назовите конструктор Worker
и передайте туда URL-адрес JavaScript-файла.
// window context app.jsconst worker = new Worker('worker.js')
// window context app.js const worker = new Worker('worker.js')
Воркер использует механизм сообщений для общения с основным потоком. Для отправки сообщения используется метод post
.
// Основной поток: app.jsconst worker = new Worker('worker.js')// Отправляем сообщение из основного потока в воркерworker.postMessage({ message: '415-ый, я база, ответьте' })
// Основной поток: app.js const worker = new Worker('worker.js') // Отправляем сообщение из основного потока в воркер worker.postMessage({ message: '415-ый, я база, ответьте' })
В глобальном контексте воркера есть обработчик onmessage
. Его можно использовать, чтобы принимать сообщения. Воркер также может отправлять сообщения в основной поток при помощи функции post
. Функцию можно вызывать в любом месте воркера.
// Воркер: worker.jsonmessage = function (e) { // Слушаем сообщения из основного потока if (e.message === '415-ый, я база, ответьте') { {/* Отправляем сообщение из воркера в основной поток */} postMessage('База, это 415-ый, как слышно?') }}
// Воркер: worker.js onmessage = function (e) { // Слушаем сообщения из основного потока if (e.message === '415-ый, я база, ответьте') { {/* Отправляем сообщение из воркера в основной поток */} postMessage('База, это 415-ый, как слышно?') } }
Чтобы получать сообщения в основном потоке, используйте метод-обработчик onmessage
объекта Worker
.
// window context app.jsconst worker = new Worker('worker.js')worker.postMessage({ message: '415-ый, я база, ответьте' })worker.onmessage = function (e) { // Слушаем сообщения из воркера console.log(e) // База, это 415-ый, как слышно?}
// window context app.js const worker = new Worker('worker.js') worker.postMessage({ message: '415-ый, я база, ответьте' }) worker.onmessage = function (e) { // Слушаем сообщения из воркера console.log(e) // База, это 415-ый, как слышно? }
Внимательный читатель заметит, что в воркер отправился объект со свойством message
, а от воркера пришла строка. В функцию post
можно передавать значения любого типа, включая объекты. Единственное ограничение — передаваемые данные должны поддерживать алгоритм структурированного клонирования.
Что доступно внутри?
СкопированоРанее упоминали, что в контексте выполнения воркера недоступны многие API из объекта window
основного потока. Что же тогда доступно? Перечислим некоторые функции API, которые часто используются: fetch
, set
, set
, request
и queue
. Для любознательных — полный список поддерживаемых API.
Типы воркеров
СкопированоВ примере выше рассмотрели первый тип воркеров — Dedicated Worker
. Он будет доступен только в том потоке, который его создал. Это может быть основной поток или поток другого воркера. Но что, если мы хотим использовать воркер в разных вкладках браузера? Для этого используют другой типа воркера — Shared Worker
.
Shared Workers позволяет создать поток, разделяемый между несколькими вкладками, iframe или окнами в пределах одного и того же происхождения (origin). Это означает, что Shared Worker может быть использован одновременно несколькими частями веб-приложения для обмена данными, синхронизации состояний или выполнения фоновых задач без необходимости повторной загрузки или дублирования в каждой вкладке или окне. Тут стоит отметить, что состояние Shared Worker будет живо, пока о нём кто-то помнит.
Разберёмся, как создать и запустить Shared Worker.
Логика схожа с логикой Dedicated Worker
, но есть несколько исключений. Во-первых, для создания Shared
нужно использовать конструктор Shared
. Во-вторых, onmessage
и post
доступны в свойстве воркера port
:
// Первая вкладка: app1.jsconst sharedWorker = new SharedWorker('worker.js')sharedWorker.port.onmessage = (event) => { console.log('data from worker', event)}const sendDataToWorker = () => { sharedWorker.port.postMessage(1)}
// Первая вкладка: app1.js const sharedWorker = new SharedWorker('worker.js') sharedWorker.port.onmessage = (event) => { console.log('data from worker', event) } const sendDataToWorker = () => { sharedWorker.port.postMessage(1) }
То же самое делаем в другой вкладке:
// Вторая вкладка: app2.jsconst sharedWorker = new SharedWorker('worker.js')sharedWorker.port.onmessage = (event) => { console.log('data from worker', event)}const sendDataToWorker = () => { sharedWorker.port.postMessage(2)}
// Вторая вкладка: app2.js const sharedWorker = new SharedWorker('worker.js') sharedWorker.port.onmessage = (event) => { console.log('data from worker', event) } const sendDataToWorker = () => { sharedWorker.port.postMessage(2) }
Код SharedWorker выглядит так:
// Воркер: worker.jslet sum = 0onconnect = (connect) => { const port = connect.ports[0] // В ports всегда один элемент port.onmessage = (event) => { sum += event } port.postMessage(sum)}
// Воркер: worker.js let sum = 0 onconnect = (connect) => { const port = connect.ports[0] // В ports всегда один элемент port.onmessage = (event) => { sum += event } port.postMessage(sum) }
Обработчик события onconnect
принимает event (мы называем его connect
). Внутри обработчика используется свойство ports
– массив в котором всегда будет один элемент. Используя свойство onmessage
объекта port можно подписаться на сообщения из других потоков. Отправка сообщения также происходит через port
.
Вложенность Web Workers
СкопированоВоркеры могут быть вложенными, и одни воркеры могут управлять другими воркерами. Работа с вложенными воркерами не отличается от работы с воркерами в основном потоке.
Импорты в Web workers
СкопированоНачиная с июня 2023 года, практически все браузеры поддерживают импорт ES-модулей в контексте воркеров. Поэтому можно использовать конструкцию import xxxxx from ‘lib’
. Эта информация пригодится вам при настройке сборки приложения.
Отправка данных в Web worker
СкопированоДанные передаваемые в post
по умолчанию копируются, что может быть медленно, особенно при передаче больших или сложных объектов.
Отправка данных без копирования
СкопированоДля оптимизации производительности и минимизации затрат на копирование данных можно использовать технику передачи данных через Transferable objects. Transferable objects не копируются, а перемещаются между контекстами. После оправки Transferable object пропадает из места откуда его отправили. Примерами transferable objects являются Array
и Message
.
Пример использования Transferable objects
СкопированоОтправка данных в веб-воркер:
// Создание ArrayBufferconst buffer = new ArrayBuffer(1024) // 1024 байта// Отправка ArrayBuffer в воркерworker.postMessage(buffer, [buffer])// теперь buffer не доступен в основном потоке
// Создание ArrayBuffer const buffer = new ArrayBuffer(1024) // 1024 байта // Отправка ArrayBuffer в воркер worker.postMessage(buffer, [buffer]) // теперь buffer не доступен в основном потоке
Прием данных в воркере:
onmessage = function(e) { const buffer = e.data // Получение ArrayBuffer // Можно начать работу с данными};
onmessage = function(e) { const buffer = e.data // Получение ArrayBuffer // Можно начать работу с данными };
В этом примере объект типа Array
отправляется в воркер через post
, массив с этим объектом также передаётся вторым аргументом как transferable object.
Обратите внимание:
- После передачи transferable object, источник теряет доступ к объекту. Это значит, что объект нельзя использовать в источнике после его отправки.
- Не все типы могут быть переданы как transferable object.
Array
,Buffer Message
точно можно перемещать.Port
Transferable objects работают быстро, так как позволяют избегать глубокого копирования. Это особенно заметно при работе с большими или сложными объектами в приложениях, требующих высокой производительности. Например в играх, графических редакторах и обработчиках видео и аудио в реальном времени.
Заключение
СкопированоВоркеры — это мощный инструмент для разработки более отзывчивых и производительных веб-приложений. Они позволяют разгрузить основной поток от тяжелых вычислений, улучшают пользовательский опыт. Однако нужно учитывать их ограничения и особенности для эффективного использования в своих проектах.