Архитектура вне основного потока может значительно повысить надежность вашего приложения и удобство работы с пользователем.
За последние 20 лет Интернет радикально изменился: от статических документов с несколькими стилями и изображениями к сложным динамическим приложениям. Однако одно осталось практически неизменным: у нас есть только один поток на каждую вкладку браузера (за некоторыми исключениями), который выполняет работу по рендерингу наших сайтов и запуску нашего JavaScript.
В результате основной поток стал невероятно перегружен. А по мере усложнения веб-приложений основной поток становится серьезным узким местом для производительности. Что еще хуже, количество времени, необходимое для выполнения кода в основном потоке для данного пользователя, почти полностью непредсказуемо, поскольку возможности устройства оказывают огромное влияние на производительность. Эта непредсказуемость будет только расти по мере того, как пользователи будут получать доступ к Интернету со все более разнообразного набора устройств, от сверхограниченных функциональных телефонов до мощных флагманских компьютеров с высокой частотой обновления.
Если мы хотим, чтобы сложные веб-приложения надежно соответствовали рекомендациям по производительности, таким как Core Web Vitals , основанным на эмпирических данных о человеческом восприятии и психологии, нам нужны способы выполнения нашего кода вне основного потока (OMT) .
Почему веб-работники?
По умолчанию JavaScript — это однопоточный язык, который выполняет задачи в основном потоке . Однако веб-работники предоставляют своего рода выход из основного потока, позволяя разработчикам запускать отдельные потоки для обработки работы за пределами основного потока. Хотя возможности веб-воркеров ограничены и они не обеспечивают прямого доступа к DOM, они могут быть чрезвычайно полезны, если необходимо выполнить значительную работу, которая в противном случае перегрузила бы основной поток.
Что касается Core Web Vitals , выполнение работы за пределами основного потока может быть полезным. В частности, перенесение работы из основного потока на веб-воркеров может уменьшить конкуренцию за основной поток, что может улучшить важные показатели реагирования, такие как взаимодействие с следующей отрисовкой (INP) и задержка первого ввода (FID) . Когда у основного потока меньше работы для обработки, он может быстрее реагировать на взаимодействия с пользователем.
Меньшая работа основного потока, особенно во время запуска, также несет потенциальную выгоду для 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 , поэтому вы не можете поместить в работника части своего приложения, которые полагаются на такой доступ. Тем не менее, каждый небольшой фрагмент кода, перенесенный в рабочий процесс, освобождает в главном потоке больше места для вещей, которые там должны быть, например обновления пользовательского интерфейса.
Одна из проблем веб-разработчиков заключается в том, что большинство веб-приложений полагаются на инфраструктуру пользовательского интерфейса, такую как Vue или React, для координации всего в приложении; все является компонентом фреймворка и поэтому по своей сути привязано к DOM. Казалось бы, это затруднит переход на архитектуру OMT.
Однако, если мы перейдем к модели, в которой проблемы пользовательского интерфейса отделены от других проблем, таких как управление состоянием, веб-работники могут быть весьма полезны даже с приложениями на основе платформы. Именно такой подход использован в PROXX.
PROXX: практический пример ОМТ
Команда Google Chrome разработала PROXX как клон Minesweeper, который соответствует требованиям Progressive Web App , включая работу в автономном режиме и привлекательный пользовательский интерфейс. К сожалению, ранние версии игры плохо работали на устройствах с ограниченными возможностями, таких как функциональные телефоны, что привело команду к выводу, что основной поток является узким местом.
Команда решила использовать веб-воркеров, чтобы отделить визуальное состояние игры от ее логики:
- Основной поток занимается рендерингом анимации и переходов.
- Веб-воркер обрабатывает игровую логику, которая является чисто вычислительной.
OMT оказал интересное влияние на производительность мобильных телефонов PROXX. В версии, отличной от OMT, пользовательский интерфейс зависает на шесть секунд после взаимодействия с ним пользователя. Обратная связь отсутствует, и пользователю приходится ждать целых шесть секунд, прежде чем он сможет сделать что-то еще.
Однако в версии OMT игре требуется двенадцать секунд для завершения обновления пользовательского интерфейса. Хотя это кажется потерей производительности, на самом деле это приводит к увеличению обратной связи с пользователем. Замедление происходит потому, что приложение отправляет больше кадров, чем версия без OMT, которая вообще не отправляет никаких кадров. Таким образом, пользователь знает, что что-то происходит, и может продолжать играть по мере обновления пользовательского интерфейса, благодаря чему игра становится значительно лучше.
Это сознательный компромисс: мы даем пользователям устройств с ограниченными возможностями удобство работы , не наказывая при этом пользователей устройств высокого класса.
Последствия архитектуры OMT
Как показывает пример PROXX, OMT обеспечивает надежную работу вашего приложения на более широком спектре устройств, но не делает его быстрее:
- Вы просто перемещаете работу из основного потока, а не сокращаете ее.
- Дополнительные затраты на связь между веб-работником и основным потоком иногда могут немного замедлить работу.
Учитывая компромиссы
Поскольку основной поток может свободно обрабатывать пользовательские взаимодействия, такие как прокрутка, во время работы JavaScript, пропущенных кадров становится меньше, хотя общее время ожидания может быть немного больше. Заставить пользователя немного подождать предпочтительнее, чем отбрасывать кадр, потому что для пропущенных кадров погрешность меньше: удаление кадра происходит за миллисекунды, в то время как у вас есть сотни миллисекунд, прежде чем пользователь поймет время ожидания.
Из-за непредсказуемости производительности на разных устройствах цель архитектуры OMT на самом деле заключается в снижении риска — повышении устойчивости вашего приложения в условиях сильно меняющихся условий выполнения, а не в преимуществах производительности от распараллеливания. Повышение устойчивости и улучшение UX более чем стоят любого небольшого компромисса в скорости.
Примечание об инструментах
Веб-воркеры еще не стали мейнстримом, поэтому большинство инструментов модулей, таких как Webpack и Rollup , не поддерживают их «из коробки». (Хотя в Parcel !) К счастью, есть плагины, которые позволяют веб-работникам работать с веб-пакетами и Rollup:
- рабочий плагин для веб-пакета
- rollup-plugin-off-main-thread для Rollup
Подведение итогов
Чтобы наши приложения были максимально надежными и доступными, особенно на растущем глобализированном рынке, нам необходимо поддерживать устройства с ограниченными возможностями — именно с их помощью большинство пользователей получают доступ к Интернету во всем мире. OMT предлагает многообещающий способ повышения производительности таких устройств без негативного воздействия на пользователей устройств высокого класса.
Кроме того, ОМТ имеет второстепенные преимущества:
- Он переносит затраты на выполнение JavaScript в отдельный поток.
- Это снижает затраты на анализ , а это означает, что пользовательский интерфейс может загружаться быстрее. Это может уменьшить количество First Contentful Paint или даже Time to Interactive , что, в свою очередь, может увеличить ваш балл Lighthouse .
Веб-работники не должны бояться. Такие инструменты, как Comlink, освобождают работников от работы и делают их жизнеспособным выбором для широкого спектра веб-приложений.
Героическое изображение из Unsplash , автор Джеймс Пикок .