Как работает JS: веб-воркеры и пять сценариев их использования
Публикуем перевод седьмой части часть серии материалов об особенностях работы различных механизмов JavaScript. Наша сегодняшняя тема — веб-воркеры. В частности, речь пойдёт о различных типах веб-воркеров, о том, как организована совместная работа тех частей, из которых они состоят, а также об их возможностях и об ограничениях, с которыми можно столкнуться в разных сценариях их использования. Здесь же будет показано 5 вариантов практического применения веб-воркеров.
Ограничения асинхронного программирования
Прежде чем мы начнём говорить о веб-воркерах, стоит вспомнить о том, что JavaScript — это однопоточный язык, однако, он поддерживает и возможности асинхронного выполнения кода.
Асинхронному программированию и вариантам его применения в JS-проектах был посвящён один из предыдущих материалов этой серии.
Асинхронное выполнение кода позволяет пользовательскому интерфейсу веб-приложений нормально функционировать, реагировать на команды пользователя. Система «планирует» нагрузку на цикл событий таким образом, чтобы в первую очередь выполнялись операции, связанные с пользовательским интерфейсом.
Хороший пример использования асинхронных методов программирования демонстрирует техника выполнения AJAX-запросов. Так как ожидание ответа способно занять много времени, запросы можно делать асинхронно, при этом, пока клиент ожидает ответа, может выполняться код, не относящийся к запросу.
Такой подход, однако, демонстрирует следующую проблему: запросы обрабатываются WEB API браузера. Нас же интересует возможность асинхронного выполнения произвольного кода. Скажем, как быть, если код внутри функции обратного вызова интенсивно использует ресурсы процессора?
Если функция performCPUIntensiveCalculation — это не нечто вроде асинхронно выполняемого HTTP-запроса, а код, блокирующий главный поток (скажем, огромный и тяжёлый цикл for ), то при однопоточном подходе к JS-разработке у нас нет способа освободить цикл событий и разблокировать интерфейс браузера. Как результат, пользователь не сможет с ним нормально работать.
Это означает, что асинхронные функции смягчают лишь небольшую часть ограничений, связанных с однопоточностью JS.
В некоторых случаях хорошего результата в деле разгрузки главного потока при выполнении ресурсоёмких операций можно достичь с помощью setTimeout . Например, если разбить сложные вычисления на фрагменты, выполняемые в разных вызовах setTimeout , их можно «распределить» по циклу событий, и таким образом, не блокировать пользовательский интерфейс.
Взглянем на простую функцию, которая вычисляет среднее значение для числового массива.
Этот код можно переписать так, чтобы он «эмулировал» асинхронное выполнение:
При таком подходе мы используем функцию setTimeout , которая планирует выполнение вычислений. Это приводит к размещению в цикле событий функции, выполняющей следующую порцию вычислений, таким образом, что между сеансами выполнения этой функции достаточно времени для других вычислений, в том числе и для тех, которые связаны с пользовательским интерфейсом.
Веб-воркеры
HTML5 дал нам множество замечательных возможностей, среди которых можно отметить следующие:
- SSE (эту технологию мы рассматривали и сравнивали с протоколом WebSocket в одном из предыдущих материалов).
- Геолокация.
- Кэш приложения.
- Локальное хранилище.
- Технология Drag and Drop.
- Веб-воркеры.
Это поистине замечательная возможность. Система понятий JavaScript основана на идее однопоточного окружения, а теперь перед нами технология, которая (частично) снимает это ограничение.
Веб-воркеры позволяют разработчику размещать задачи, для выполнения которых требуются длительные и сложные вычисления, интенсивно задействующие процессор, в фоновых потоках, без блокировки пользовательского интерфейса, что позволяет приложениям оперативно реагировать на воздействия пользователя. Более того, нам больше не нужны обходные пути, вроде рассмотренного выше трюка с setTimeout для того, чтобы найти приемлемый способ взаимодействия с циклом событий.
Вот простой пример, который демонстрирует разницу между сортировкой массива с помощью веб-воркера и без него.
▍Обзор веб-воркеровВеб-воркеры позволяют выполнять тяжёлые в вычислительном плане и длительные задачи без блокировки потока пользовательского интерфейса. На самом деле, при их использовании вычисления выполняются параллельно. Перед нами настоящая многопоточность.
Возможно, вы вспомните о том, что JavaScript — это однопоточный язык программирования. Пожалуй, тут вы должны осознать, что JS — это язык, который не определяет модель потоков. Веб-воркеры не являются частью JavaScript. Они представляют собой возможность браузера, к которой можно получить доступ посредством JavaScript. Большинство браузеров исторически были однопоточными (эта ситуация, конечно, изменилась), и большинство реализаций JavaScript создано для браузеров.
Веб-воркеры не реализованы в Node.js — там есть концепция «кластеров» или «дочерних процессов», а это уже немного другое.
Стоит отметить, что спецификация упоминает три типа веб-воркеров:
- Выделенные воркеры (Dedicated Workers)
- Разделяемые воркеры (Shared Workers)
- Сервис-воркеры (Service Workers)
Экземпляры выделенных веб-воркеров создаются главным процессом. Обмениваться данными с ними может только он.
Поддержка выделенных воркеров в браузерах
▍Разделяемые воркерыДоступ к разделяемому воркеру может получить любой процесс, имеющий тот же источник, что и воркер (например — разные вкладки браузера, iframe , и другие разделяемый воркеры).
Поддержка разделяемых воркеров в браузерах
▍Сервис-воркерыСервис-воркеры — это воркеры, управляемые событиями, зарегистрированные с использованием источника их происхождения и пути. Они могут контролировать веб-страницу, с которой связаны, перехватывая и модифицируя команды навигации и запросы ресурсов, и выполняя кэширование данных, которым можно очень точно управлять. Всё это даёт нам отличные средства управления поведением приложения в определённой ситуации (например, когда сеть недоступна).
Поддержка сервис-воркеров в браузерах
Надо отметить, что в этом материале мы будем заниматься выделенными воркерами, именно их мы будем иметь в виду, говоря о «веб-воркерах» или о «воркерах».
Как работают веб-воркеры
Веб-воркеры реализованы с использованием .js-файлов, которые включаются в страницу с применением асинхронного HTTP-запроса. Эти запросы полностью скрыты от разработчика благодаря Web Worker API.
Воркеры используют механизмы передачи сообщений, характерные для технологий, которые применяются для организации взаимодействия потоков, что позволяет организовать их параллельное выполнение. Они отлично подходят для того, чтобы выполнять тяжёлые вычислительные операции, не замедляя работу пользовательского интерфейса.
Веб-воркеры выполняются в изолированных потоках в браузере. Как результат, код, который они выполняют, должен быть включён в отдельный файл. Это важно запомнить.
Вот как создают веб-воркеры:
Если файл task.js существует и к нему есть доступ, браузер создаст новый поток, который асинхронно загрузит этот файл. После того, как загрузка будет завершена, начнётся выполнение кода воркера. Если при попытке загрузки файла браузер получит сообщение об ошибке 404, файл загружен не будет, при этом сообщения об ошибках не выводятся.
Для запуска только что созданного воркера нужно вызвать его метод postMessage :
Обмен данными с веб-воркером
Для того чтобы страница, создавшая веб-воркер, могла взаимодействовать с ним, нужно использовать либо метод postMessage , либо широковещательный канал передачи данных, представленный объектом BroadcastChannel.
▍Метод postMessageПри вызове этого метода более новые браузеры поддерживают, в качестве первого параметра, объект JSON, а в более старых браузерах поддерживается лишь параметр типа String .
Посмотрим на пример того, как страница, создавшая воркер, может обмениваться с ним данными, используя JSON-объект. При передаче строки выглядит всё практически точно так же.
Вот часть HTML-страницы:
Вот содержимое файла с кодом воркера:
Когда нажимают на кнопку, на странице выполняется вызов метода postMessage воркера. Этот вызов передаёт воркеру JSON-объект с ключами cmd и data и соответствующими им значениями. Воркер обработает это сообщение посредством заданного в нём обработчика message .
Когда воркер получает сообщение и понимает, чего от него хотят, он будет выполнять вычисления самостоятельно, не блокируя цикл событий. То, чем занимается воркер, выглядит как стандартная JS-функция. Когда вычисления завершены, их результаты передаются главной странице.
В контексте воркера и self , и this , указывают на глобальное пространство имён для воркера.
Для того чтобы остановить воркер, можно воспользоваться одним из двух способов. Первый заключается в вызове с главной страницы метода worker.terminate() . Второй выполняется внутри воркера и реализуется командой self.close() .
▍Широковещательный канал передачи данныхОбъект BroadcastChannel представляет собой более универсальное API для передачи данных. Он позволяет передавать сообщения, которые можно принять во всех контекстах, имеющих один и тот же источник. Все вкладки браузера, iframe или воркеры, относящиеся к одному источнику, могут передавать и принимать широковещательные сообщения:
Вот как выглядит схема взаимодействия различных сущностей с использованием широковещательного канала обмена сообщениями:
Обмен данными с использованием широковещательного канала передачи сообщений
Однако тут стоит отметить, что объект BroadcastChannel пока имеет довольно ограниченную поддержку в браузерах.
Поддержка BroadcastChannel в браузерах
Способы отправки сообщений веб-воркерам
Есть два способа отправки сообщений веб-воркерам. Первый заключается в копировании данных, второй — в передаче данных от источника к приёмнику без их копирования. Рассмотрим эти способы работы с сообщениями:
- Копирование сообщения. Сообщение сериализуется, копируется, отправляется, а затем, на принимающей стороне, десериализуется. Страница и воркер не используют общий экземпляр сообщения, поэтому тут мы сталкиваемся с созданием копий данных в каждом сеансе отправки сообщений. Большинство браузеров реализуют эту возможность путём автоматического преобразования передаваемой информации в JSON на стороне передатчика и декодирования этих данных на стороне приёмника. Как можно ожидать, это добавляет значительную нагрузку на систему при отправке сообщений. Чем больше сообщение — тем больше времени займёт его отправка.
- Передача сообщения. При таком подходе оказывается, что отправитель сообщения больше не может использовать сообщение после того, как оно отправлено. При этом передача сообщений выполняется практически мгновенно. Главная особенность этого метода заключается в том, что передать с его помощью можно только объект ArrayBuffer.
Возможности, доступные веб-воркерам
Веб-воркерам, из-за их многопоточной сущности, доступен лишь ограниченный набор возможностей JavaScript. Вот эти возможности:
- Объект navigator
- Объект location (только для чтения)
- XMLHttpRequest
- setTimeout()/clearTimeout() и setInterval()/clearInterval()
- Импорт внешних скриптов с использованием importScripts() других веб-воркеров
Ограничения веб-воркеров
К сожалению, у веб-воркеров нет доступа к некоторым весьма важным возможностям JavaScript. Среди них следующие:
- DOM (это не потокобезопасно)
- Объект window
- Объект document
- Объект parent
Обработка ошибок
Как и при работе с любым JS-кодом, в веб-воркерах нужно обрабатывать ошибки. Если ошибка возникает в процессе выполнения воркера, вызывается событие ErrorEvent . Объект ошибки содержит три полезных свойства, которые позволяют понять её суть:
- filename — имя файла, в котором содержится скрипт воркера, вызвавший ошибку.
- lineno — номер строки, в которой произошла ошибка.
- message — описание ошибки.
Вот код воркера
Тут вы можете видеть, как мы создали воркер и назначили ему обработчик события error .
Внутри воркера (второй фрагмент кода) мы намеренно вызываем исключение, умножая x на 2 в то время как x не определено в текущей области видимости. Исключение доходит до исходного скрипта и вызывается обработчик onError , выводящий сведения об ошибке.
Сценарии использования веб-воркеров
Мы рассказали о сильных и слабых сторонах веб-воркеров. Теперь рассмотрим несколько сценариев их использования.
- Рендеринг трёхмерных сцен. В частности, речь идёт о реализации метода трассировки лучей — техники рендеринга, позволяющей создавать изображения путём отслеживания направления лучей света и определения цвета пикселей. Трассировка лучей использует интенсивные математические вычисления для моделирования особенностей распространения света. При таком подходе реализуются такие эффекты, как отражения и преломления, трассировка лучей позволяет добиться имитировать внешний вид различных материалов, и так далее. Вся эта вычислительная логика может быть вынесена в веб-воркер для того, чтобы она не блокировала поток пользовательского интерфейса. Можно сделать ещё интереснее, а именно, разделить рендеринг изображения между несколькими воркерами (и, соответственно, между несколькими процессорными ядрами). Вот простой пример реализации трассировки лучей с использованием веб-воркеров.
- Шифрование. Сквозное шифрование становится всё более популярным из за всё возрастающего внимания к регулированию распространения персональных и конфиденциальных данных. Операции шифрования могут быть достаточно продолжительными, особенно если возникает необходимость в частом шифровании больших объёмов данных. Это — весьма адекватный сценарий использования веб-воркера, так как тут не нужен доступ к объектам DOM или нечто подобное. Шифрование — это алгоритмы обработки информации, которым достаточно базовых возможностей JS. Когда шифрование выполняется воркером, это не влияет на работу пользователя с интерфейсом сайта.
- Предварительная загрузка данных. Для того чтобы оптимизировать веб-сайт и улучшить впечатления пользователя от работы с ним, можно использовать веб-воркеры для заблаговременной загрузки и сохранения некоторых данных, которыми можно очень быстро воспользоваться тогда, когда позже в них возникнет необходимость. Веб-воркеры отлично подходят для подобного сценария использования, так как выполняемые ими операции не подействуют на интерфейс приложения, в отличие от предварительной загрузки данных, реализованной средствами главного потока.
- Прогрессивные веб-приложения. Такие приложения должны, даже при ненадёжном сетевом соединении, быстро загружаться. Это означает, что данные нужно хранить в браузере локально. Именно здесь в дело вступает IndexedDB или похожее API. В целом, речь идёт о необходимости обслуживания некоего хранилища данных на стороне клиента. Для того чтобы работать с этим хранилищем, не блокируя пользовательский интерфейс, работу надо организовать в веб-воркере. Тут надо отметить, что, в случае с IndexedDB, существует асинхронное API, которое позволяет не нагружать главный поток и без веб-воркеров, но раньше здесь было синхронное API (которое может появиться снова), которым нужно пользоваться только внутри веб-воркеров.
- Проверка правописания. Простая система проверки правописания работает так: программа считывает файл словаря со списком правильно написанных слов. Из словаря формируется дерево поиска, которое обеспечивает эффективный поиск по тексту. Когда системе передают слово для проверки, она проверяет его наличие в дереве поиска. Если слово найти не удаётся, пользователю могут быть предоставлены альтернативные варианты этого слова, полученные путём замены символов исходного слова и поиска полученных слов в дереве на предмет проверки того, являются ли они, с точки зрения системы проверки, правильными. Всё это легко можно вынести в веб-воркер, что даст пользователю возможность работать с текстом, не испытывая проблем, связанных с блокировкой интерфейса при проверке слова и при поиске альтернативных вариантов его написания.
Итоги
В этом материале мы рассказали о веб-воркерах — сравнительно новой возможности, доступной веб-разработчикам в большинстве современных браузеров. Веб-воркеры позволяют выносить в отдельные потоки выполнение ресурсоёмких операций, что позволяет не нагружать главный поток, который может спокойно обрабатывать всё, что связано с пользовательским интерфейсом.
Предыдущие части цикла статей:
Уважаемые читатели! Используете ли вы веб-воркеры в своих проектах?