Архитектура, использующая отдельный поток вне основного приложения, может значительно повысить его надежность и удобство использования.
За последние 20 лет веб-технологии претерпели колоссальную эволюцию: от статических документов с несколькими стилями и изображениями до сложных, динамических приложений. Однако одно осталось практически неизменным: на каждую вкладку браузера (с некоторыми исключениями) приходится всего один поток, выполняющий работу по отрисовке сайтов и запуску JavaScript.
В результате основной поток стал невероятно перегружен. А по мере роста сложности веб-приложений основной поток становится существенным узким местом для производительности. Что еще хуже, время выполнения кода в основном потоке для конкретного пользователя практически непредсказуемо, поскольку возможности устройства оказывают огромное влияние на производительность. Эта непредсказуемость будет только расти по мере того, как пользователи будут получать доступ к веб-сайтам с все более разнообразного набора устройств, от сверхмощных телефонов до высокопроизводительных флагманских машин с высокой частотой обновления экрана.
Если мы хотим, чтобы сложные веб-приложения надежно соответствовали требованиям к производительности, таким как Core Web Vitals , основанным на эмпирических данных о человеческом восприятии и психологии, нам нужны способы выполнения кода вне основного потока (OMT) .
Почему именно веб-воркеры?
JavaScript по умолчанию является однопоточным языком, выполняющим задачи в основном потоке . Однако веб-воркеры предоставляют своего рода выход из основного потока, позволяя разработчикам создавать отдельные потоки для обработки задач вне основного потока. Хотя возможности веб-воркеров ограничены и они не предоставляют прямого доступа к DOM, они могут быть чрезвычайно полезны, если необходимо выполнить значительный объем работы, который в противном случае перегрузил бы основной поток.
Что касается основных параметров веб-процесса (Core Web Vitals ), то перенос задач из основного потока может быть полезен. В частности, перераспределение задач из основного потока в веб-воркеры может уменьшить конкуренцию за основной поток, что может улучшить показатель отзывчивости страницы по метрике «Взаимодействие до следующей отрисовки» (INP) . Когда у основного потока меньше задач для обработки, он может быстрее реагировать на действия пользователя.
Меньшее количество работы в основном потоке — особенно во время запуска — также потенциально выгодно для Largest Contentful Paint (LCP) за счет сокращения длительных задач. Рендеринг элемента LCP требует времени основного потока — будь то для рендеринга текста или изображений, которые являются частыми и распространенными элементами LCP, — и, сокращая общую нагрузку на основной поток, вы можете гарантировать, что элемент LCP вашей страницы с меньшей вероятностью будет заблокирован дорогостоящей работой, которую мог бы выполнить веб-воркер.
Многопоточность с использованием веб-воркеров
Другие платформы обычно поддерживают параллельную работу, позволяя передавать потоку функцию, которая выполняется параллельно с остальной частью программы. Вы можете обращаться к одним и тем же переменным из обоих потоков, а доступ к этим общим ресурсам может быть синхронизирован с помощью мьютексов и семафоров для предотвращения состояний гонки.
В JavaScript мы можем получить примерно аналогичную функциональность от веб-воркеров, которые существуют с 2007 года и поддерживаются всеми основными браузерами с 2012 года. Веб-воркеры работают параллельно с основным потоком, но, в отличие от многопоточности операционной системы, они не могут совместно использовать переменные.
Для создания веб-воркера передайте файл в конструктор воркера, который запустит выполнение этого файла в отдельном потоке:
const worker = new Worker("./worker.js");
Взаимодействуйте с веб-воркером, отправляя сообщения с помощью API postMessage . Передайте значение сообщения в качестве параметра в вызове postMessage , а затем добавьте обработчик событий сообщений к воркеру:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
// ...
});
Чтобы отправить сообщение обратно в основной поток, используйте тот же API postMessage в веб-воркере и настройте обработчик событий в основном потоке:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
console.log(event.data);
});
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
postMessage(a + b);
});
Следует признать, что такой подход несколько ограничен. Исторически веб-воркеры в основном использовались для переноса одной ресурсоемкой задачи из основного потока. Попытка обрабатывать несколько операций с помощью одного веб-воркера быстро становится громоздкой: необходимо кодировать в сообщении не только параметры, но и саму операцию, а также вести учет для сопоставления ответов с запросами. Вероятно, именно эта сложность является причиной того, почему веб-воркеры не получили более широкого распространения.
Но если бы мы смогли упростить взаимодействие между основным потоком и веб-воркерами, эта модель могла бы отлично подойти для многих сценариев использования. И, к счастью, существует библиотека, которая делает именно это!
Comlink: сокращение трудозатрат на веб-работников
Comlink — это библиотека, цель которой — позволить вам использовать веб-воркеры, не задумываясь о деталях postMessage . Comlink позволяет обмениваться переменными между веб-воркерами и основным потоком почти так же, как и в других языках программирования, поддерживающих многопоточность.
Для настройки Comlink необходимо импортировать его в веб-воркер и определить набор функций, которые будут доступны основному потоку. Затем вы импортируете Comlink в основном потоке, оборачиваете воркер и получаете доступ к доступным функциям:
worker.js
import {expose} from 'comlink';
const api = {
someMethod() {
// ...
}
}
expose(api);
main.js
import {wrap} from 'comlink';
const worker = new Worker('./worker.js');
const api = wrap(worker);
Переменная api в основном потоке ведет себя так же, как и в веб-воркере, за исключением того, что каждая функция возвращает промис со значением, а не само значение.
Какой код следует перенести в веб-воркер?
Веб-воркеры не имеют доступа к DOM и многим API, таким как WebUSB , WebRTC или Web Audio , поэтому вы не можете поместить в ворк части вашего приложения, которые зависят от такого доступа. Тем не менее, каждый небольшой фрагмент кода, перемещенный во воркер, увеличивает запас ресурсов в основном потоке для задач, которые должны выполняться — например, для обновления пользовательского интерфейса.
Одна из проблем для веб-разработчиков заключается в том, что большинство веб-приложений полагаются на UI-фреймворки, такие как Vue или React, для управления всем процессом приложения; всё является компонентом фреймворка и, следовательно, неразрывно связано с DOM. Это, по-видимому, затрудняет переход к архитектуре OMT.
Однако, если мы перейдем к модели, в которой задачи пользовательского интерфейса отделены от других задач, таких как управление состоянием, веб-воркеры могут быть весьма полезны даже для приложений, основанных на фреймворках. Именно такой подход используется в PROXX.
PROXX: пример из практики OMT
Команда Google Chrome разработала PROXX как клон «Сапёра», отвечающий требованиям прогрессивных веб-приложений (PEB) , включая работу в автономном режиме и привлекательный пользовательский интерфейс. К сожалению, ранние версии игры плохо работали на устройствах с ограниченными ресурсами, таких как обычные мобильные телефоны, что привело команду к выводу, что основным узким местом является основной поток.
Команда решила использовать веб-воркеры, чтобы отделить визуальное состояние игры от её логики:
- Основной поток отвечает за отрисовку анимаций и переходов.
- Веб-воркер обрабатывает игровую логику, которая является чисто вычислительной.
Технология OMT оказала интересное влияние на производительность PROXX в режиме обычного телефона. В версии без OMT пользовательский интерфейс зависает на шесть секунд после взаимодействия пользователя с ним. Обратная связь отсутствует, и пользователю приходится ждать все шесть секунд, прежде чем он сможет сделать что-то еще.
Однако в версии OMT обновление пользовательского интерфейса занимает двенадцать секунд. Хотя это кажется снижением производительности, на самом деле это приводит к улучшению обратной связи с пользователем. Замедление происходит потому, что приложение отправляет больше кадров, чем версия без OMT, которая вообще не отправляет кадров. Таким образом, пользователь знает, что что-то происходит, и может продолжать играть, пока обновляется интерфейс, что делает игру значительно приятнее.
Это осознанный компромисс: мы предоставляем пользователям устройств с ограниченными возможностями более удобный интерфейс, не наказывая при этом пользователей устройств высокого класса.
Последствия архитектуры OMT
Как показывает пример PROXX, OMT обеспечивает надежную работу вашего приложения на более широком спектре устройств, но не ускоряет его работу:
- Вы просто переносите работу из основного потока, а не уменьшаете её объём.
- Дополнительные накладные расходы на обмен данными между веб-воркером и основным потоком иногда могут незначительно замедлять работу системы.
Рассмотрите компромиссы.
Поскольку основной поток свободен для обработки взаимодействий с пользователем, таких как прокрутка, во время выполнения JavaScript, количество пропущенных кадров уменьшается, даже несмотря на то, что общее время ожидания может быть немного больше. Заставить пользователя немного подождать предпочтительнее, чем пропустить кадр, потому что погрешность при пропущенных кадрах меньше: пропущенный кадр происходит за миллисекунды, в то время как у вас есть сотни миллисекунд, прежде чем пользователь воспримет время ожидания.
Из-за непредсказуемости производительности на разных устройствах цель архитектуры OMT заключается скорее в снижении рисков — повышении устойчивости приложения к сильно изменяющимся условиям выполнения — а не в повышении производительности за счет распараллеливания. Повышение отказоустойчивости и улучшение пользовательского опыта с лихвой компенсируют любой небольшой компромисс в скорости.
Примечание об инструментах
Веб-воркеры пока не получили широкого распространения, поэтому большинство инструментов для работы с модулями, таких как webpack и Rollup, не поддерживают их по умолчанию. (Зато Parcel поддерживает!) К счастью, существуют плагины, позволяющие веб-воркерам работать с webpack и Rollup:
- worker-plugin для webpack
- rollup-plugin-off-main-thread for Rollup
Подводя итог
Чтобы обеспечить максимальную надежность и доступность наших приложений, особенно на все более глобализированном рынке, нам необходимо поддерживать устройства с ограниченными ресурсами — именно так большинство пользователей по всему миру получают доступ к интернету. OMT предлагает перспективный способ повышения производительности на таких устройствах без негативного влияния на пользователей высокопроизводительных устройств.
Кроме того, ОМТ имеет и другие преимущества:
- Это переносит затраты на выполнение JavaScript в отдельный поток.
- Это смещает затраты на обработку данных , а значит, пользовательский интерфейс может загружаться быстрее. Это может уменьшить время первого отображения контента или даже время до интерактивности , что, в свою очередь, может повысить ваш рейтинг в Lighthouse .
Веб-работники не обязательно должны быть страшными. Такие инструменты, как Comlink, облегчают работу веб-работников и делают их жизнеспособным вариантом для широкого спектра веб-приложений.