Архитектура, в которой задействованы дополнительные потоки, может значительно повысить надежность приложения и удобство его использования.
За последние 20 лет интернет сильно изменился. На смену статичным документам с несколькими стилями и изображениями пришли сложные динамические приложения. Однако одна вещь осталась практически неизменной: у нас есть только один поток на вкладку браузера (за некоторыми исключениями) для выполнения работы по рендерингу наших сайтов и запуску нашего JavaScript.
В результате основной поток оказался перегружен. По мере усложнения веб-приложений основной поток становится узким местом для производительности. Кроме того, время, необходимое для выполнения кода в основном потоке для определенного пользователя, почти невозможно предсказать, поскольку возможности устройства сильно влияют на производительность. Эта непредсказуемость будет только расти по мере того, как пользователи будут выходить в интернет со все более разнообразных устройств, от кнопочных телефонов с ограниченными возможностями до мощных флагманских компьютеров с высокой частотой обновления.
Чтобы сложные веб-приложения соответствовали рекомендациям по производительности, таким как Core Web Vitals, которые основаны на эмпирических данных о восприятии и психологии человека, нам нужны способы выполнять код вне основного потока.
Зачем нужны веб-воркеры?
По умолчанию JavaScript – это однопоточный язык, который выполняет задачи в основном потоке. Однако веб-воркеры предоставляют своего рода аварийный люк из основного потока, позволяя разработчикам создавать отдельные потоки для обработки работы вне основного потока. Хотя область действия веб-воркеров ограничена и не предоставляет прямого доступа к DOM, они могут быть чрезвычайно полезны, если необходимо выполнить значительный объем работы, который в противном случае перегрузил бы основной поток.
Если вас интересуют основные интернет-показатели, то вам может быть полезно перенести выполнение задач из основного потока. В частности, перенос работы из основного потока в веб-воркеры может снизить конкуренцию за основной поток, что может улучшить показатель отклика страницы Interaction to Next Paint (INP). Чем меньше задач у основного потока, тем быстрее он может реагировать на действия пользователя.
Уменьшение нагрузки на основной поток, особенно при запуске, также может улучшить показатель Largest Contentful Paint (LCP) за счет сокращения длительных задач. Для отрисовки элемента LCP (обычно это текст или изображение) требуется время основного потока. Если вы сократите объем работы основного потока, то снизите вероятность того, что отрисовка элемента LCP будет заблокирована ресурсоемкими задачами, которые можно передать веб-воркеру.
Потоки с веб-воркерами
Другие платформы обычно поддерживают параллельную работу, позволяя вам передать потоку функцию, которая выполняется параллельно с остальной частью вашей программы. Вы можете получить доступ к одним и тем же переменным из обоих потоков, а доступ к этим общим ресурсам можно синхронизировать с помощью блокировок и семафоров, чтобы предотвратить состояние гонки.
В JavaScript примерно такую же функциональность можно получить от веб-воркеров, которые существуют с 2007 года и поддерживаются всеми основными браузерами с 2012 года. Веб-воркеры работают параллельно с основным потоком, но, в отличие от потоков ОС, не могут использовать общие переменные.
Чтобы создать веб-воркер, передайте файл конструктору воркера, который запустит этот файл в отдельном потоке:
const worker = new Worker("./worker.js");
Обменивайтесь сообщениями с веб-воркером, используя postMessage API. Передайте значение сообщения в качестве параметра в вызове 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: пример использования OMT
Команда Google Chrome разработала PROXX – клон игры "Сапер", который соответствует требованиям к прогрессивным веб-приложениям, в том числе работает в офлайн-режиме и предлагает увлекательный игровой процесс. К сожалению, ранние версии игры плохо работали на устройствах с ограниченными возможностями, таких как кнопочные телефоны, что привело команду к пониманию того, что основной поток был узким местом.
Команда решила использовать веб-воркеры, чтобы отделить визуальное состояние игры от ее логики:
- Основной поток обрабатывает отрисовку анимации и переходов.
- Веб-воркер обрабатывает логику игры, которая является чисто вычислительной.
OMT оказал интересное влияние на производительность PROXX на мобильных телефонах. В версии без OMT интерфейс зависает на шесть секунд после взаимодействия с ним. Пользователь не получает никакой обратной связи и вынужден ждать все шесть секунд, прежде чем сможет что-то сделать.
В версии с OMT на это уходит 12 секунд. Хотя это может показаться снижением производительности, на самом деле это приводит к увеличению обратной связи с пользователем. Замедление происходит потому, что приложение отправляет больше кадров, чем версия без OMT, которая вообще не отправляет кадры. Пользователь понимает, что происходит, и может продолжать играть, пока обновляется интерфейс. Это значительно улучшает впечатление от игры.
Это осознанный компромисс: мы даем пользователям устройств с ограниченными возможностями то, что кажется лучше, не наказывая пользователей высококлассных устройств.
Последствия использования архитектуры OMT
Как показывает пример PROXX, OMT позволяет вашему приложению надежно работать на более широком диапазоне устройств, но не делает ваше приложение быстрее:
- Вы просто переносите работу из основного потока, а не сокращаете ее.
- Дополнительные затраты на связь между веб-воркером и основным потоком могут иногда немного замедлять работу.
Оцените преимущества и недостатки
Поскольку основной поток может обрабатывать действия пользователя, например прокрутку, во время выполнения JavaScript, количество пропущенных кадров уменьшается, хотя общее время ожидания может немного увеличиться. Лучше заставить пользователя немного подождать, чем пропустить кадр, потому что допустимая погрешность для пропущенных кадров меньше: пропуск кадра происходит за миллисекунды, а пользователь замечает время ожидания только через сотни миллисекунд.
Поскольку производительность на разных устройствах непредсказуема, цель архитектуры OMT – снизить риски, то есть сделать приложение более надежным в условиях высокой вариативности среды выполнения, а не повысить производительность за счет параллелизации. Повышение устойчивости и улучшение UX стоят небольшого компромисса в скорости.
Примечание об инструментах
Веб-воркеры пока не получили широкого распространения, поэтому большинство инструментов для работы с модулями, например webpack и Rollup, не поддерживают их по умолчанию. (Parcel does though!) К счастью, есть плагины, которые позволяют веб-воркерам работать с webpack и Rollup:
- worker-plugin для webpack;
- rollup-plugin-off-main-thread для Rollup;
Подведение итогов
Чтобы наши приложения были максимально надежными и доступными, особенно на все более глобализирующемся рынке, нам необходимо поддерживать устройства с ограниченными возможностями – именно с их помощью большинство пользователей во всем мире получают доступ к Интернету. OMT позволяет повысить производительность на таких устройствах, не влияя на пользователей устройств высокого класса.
Кроме того, у OMT есть дополнительные преимущества:
- Она переносит выполнение JavaScript в отдельный поток.
- Это позволяет снизить затраты на синтаксический анализ, а значит, интерфейс может загружаться быстрее. Это может сократить время отрисовки первой части контента или даже время загрузки для взаимодействия, что, в свою очередь, может повысить оценку Lighthouse.
Веб-воркеры не должны быть страшными. Такие инструменты, как Comlink, упрощают работу с воркерами и делают их подходящим выбором для широкого спектра веб-приложений.