Web Workers

Улучшаем производительность приложений при помощи Web Workers и переносим сложные задачи в фоновые потоки, чтобы не замедлять пользовательский интерфейс.

Время чтения: 7 мин

Что такое Web worker?

Скопировано

Web worker — это API, которое позволяет выполнять код вне основного потока. Благодаря этому долгие или сложные вычисления, которые выполняются на воркерах, не блокируют пользовательский интерфейс (UI).

Веб воркер создаётся в основном потоке. При создании воркеру передаётся URL-адрес скрипта. После загрузки создаётся отдельный поток, в котором выполнится скрипт воркера.

У скрипта будет свой собственный контекст, отличный от контекста окна window. В основном потоке глобальный контекст привязывается к переменной window, а в воркере — к переменной self. Контекст выполнения веб воркера WorkerGlobalScope отличается от контекста выполнения основного потока. У него нет доступа к объекту документа document и DOM API.

Особенности потока выполнения

Скопировано

Поток, в котором выполняется код воркера, изолирован от основного. В Chromium каждому из этих потоков соответствует свой собственный экземпляр движка JavaScript. Из-за этого создание нового воркера считается «тяжёлой» операцией. Обычно предполагают, что воркеров будет немного, и они будут долго жить.

Потоки могут общаться между собой через отправку сообщений. Используйте для отправки сообщений функцию postMessage().

Как создать и запустить?

Скопировано

Всё просто: назовите конструктор Worker и передайте туда URL-адрес JavaScript-файла.

        
          
          // window context app.jsconst worker = new Worker('worker.js')
          // window context app.js
const worker = new Worker('worker.js')

        
        
          
        
      

Воркер использует механизм сообщений для общения с основным потоком. Для отправки сообщения используется метод postMessage().

        
          
          // Основной поток: app.jsconst worker = new Worker('worker.js')// Отправляем сообщение из основного потока в воркерworker.postMessage({ message: '415тый, я база, ответьте' })
          // Основной поток: app.js
const worker = new Worker('worker.js')

// Отправляем сообщение из основного потока в воркер
worker.postMessage({ message: '415тый, я база, ответьте' })

        
        
          
        
      

В глобальном контексте воркера есть обработчик onmessage(). Его можно использовать, чтобы принимать сообщения. Воркер также может отправлять сообщения в основной поток при помощи функции postMessage(). Функцию можно вызывать в любом месте воркера.

        
          
          // Воркер: 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, а от воркера пришла строка. В функцию postMessage() можно передавать значения любого типа, включая объекты. Единственное ограничение — передаваемые данные должны поддерживать алгоритм структурированного клонирования.

Что доступно внутри?

Скопировано

Ранее упоминали, что в контексте выполнения воркера недоступны многие API из объекта window основного потока. Что же тогда доступно? Перечислим некоторые функции API, которые часто используются: fetch(), setInterval(), setTimeout(), requestAnimationFrame() и queueMicrotask(). Для любознательных — полный список поддерживаемых API.

Типы воркеров

Скопировано

В примере выше рассмотрели первый тип воркеров — Dedicated Worker. Он будет доступен только в том потоке, который его создал. Это может быть основной поток или поток другого воркера. Но что, если мы хотим использовать воркер в разных вкладках браузера? Для этого используют другой типа воркера — Shared Worker.

Shared Worker

Скопировано

Shared Workers позволяет создать поток, разделяемый между несколькими вкладками, iframe или окнами в пределах одного и того же происхождения (origin). Это означает, что Shared Worker может быть использован одновременно несколькими частями веб-приложения для обмена данными, синхронизации состояний или выполнения фоновых задач без необходимости повторной загрузки или дублирования в каждой вкладке или окне. Тут стоит отметить, что состояние Shared Worker будет живо, пока о нём кто-то помнит.

Разберёмся, как создать и запустить Shared Worker.

Логика схожа с логикой Dedicated Worker, но есть несколько исключений. Во-первых, для создания SharedWorker нужно использовать конструктор SharedWorker. Во-вторых, onmessage и postMessage доступны в свойстве воркера 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

Скопировано

Данные передаваемые в postMessage() по умолчанию копируются, что может быть медленно, особенно при передаче больших или сложных объектов.

Отправка данных без копирования

Скопировано

Для оптимизации производительности и минимизации затрат на копирование данных можно использовать технику передачи данных через Transferable objects. Transferable objects не копируются, а перемещаются между контекстами. После оправки Transferable object пропадает из места откуда его отправили. Примерами transferable objects являются ArrayBuffer и MessagePort.

Пример использования 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
  // Можно начать работу с данными
};


        
        
          
        
      

В этом примере объект типа ArrayBuffer отправляется в воркер через postMessage, массив с этим объектом также передаётся вторым аргументом как transferable object.

Обратите внимание:

  • После передачи transferable object, источник теряет доступ к объекту. Это значит, что объект нельзя использовать в источнике после его отправки.
  • Не все типы могут быть переданы как transferable object. ArrayBuffer, MessagePort точно можно перемещать.

Transferable objects работают быстро, так как позволяют избегать глубокого копирования. Это особенно заметно при работе с большими или сложными объектами в приложениях, требующих высокой производительности. Например в играх, графических редакторах и обработчиках видео и аудио в реальном времени.

Заключение

Скопировано

Воркеры это мощный инструмент для разработки более отзывчивых и производительных веб-приложений. Они позволяют разгрузить основной поток от тяжелых вычислений, улучшают пользовательский опыт. Однако нужно учитывать их ограничения и особенности для эффективного использования в своих проектах.