Прослушиватели событий и веб-воркеры +23


Недавно я разбирался с API Web Workers. Очень жаль, что я не уделил время этому отлично поддерживаемому инструменту раньше. Современные веб-приложения очень требовательны к возможностям главного потока выполнения JavaScript. Это воздействует на производительность проектов и на их возможности по обеспечению удобной работы пользователей. Веб-воркеры — это именно то, что в наши дни способно помочь разработчику в деле создания быстрых и удобных веб-проектов.



Момент, когда я всё понял


У веб-воркеров много положительных качеств. Но я по-настоящему осознал их полезность, столкнувшись с ситуацией, когда в некоем приложении используется несколько прослушивателей событий DOM. Таких, как события отправки формы, изменения размеров окна, щелчков по кнопкам. Все эти прослушиватели должны работать в главном потоке. Если же главный поток перегружен некими операциями, на выполнение которых нужно продолжительное время, это плохо отражается на скорости реакции прослушивателей событий на воздействия пользователей. Приложение «подтормаживает», события ждут освобождения главного потока.

Надо признать, что причина, по которой меня так заинтересовали именно прослушиватели событий, заключается в том, что я изначально неправильно понимал то, на решение каких задач рассчитаны веб-воркеры. Сначала я думал, что они могут помочь в деле повышения скорости выполнения кода. Я полагал, что приложение сможет сделать гораздо больше за некий отрезок времени в том случае, если какие-то фрагменты его кода будут выполняться параллельно, в отдельных потоках. Но в ходе выполнения кода веб-проектов весьма распространена ситуация, когда, прежде чем начать что-то делать, нужно дождаться некоего события. Скажем, DOM нужно обновить только после того, как завершатся некие вычисления. Я, зная это, наивно полагал, что, если мне, в любом случае, придётся ждать, это значит, что нет смысла переносить выполнение некоего кода в отдельный поток.

Вот пример кода, который тут можно вспомнить:

const calculateResultsButton = document.getElementById('calculateResultsButton');
const openMenuButton = document.getElementById('#openMenuButton');
const resultBox = document.getElementById('resultBox');

calculateResultsButton.addEventListener('click', (e) => {
    // "Зачем переносить это в веб-воркер, если, в любом случае, 
    // нельзя обновить DOM до завершения вычислений?"
    const result = performLongRunningCalculation();
    resultBox.innerText = result;
});

openMenuButton.addEventListener('click', (e) => {
    // Выполнить некие действия для открытия меню. 
});

Тут я обновляю текст в поле после того, как завершатся некие вычисления, предположительно — длительные. Вроде бы бессмысленно запускать этот код в отдельном потоке, так как DOM не обновить раньше, чем завершится выполнение этого кода. В результате я, конечно, решаю, что код этот нужно выполнять синхронно. Правда, видя подобный код, я сначала не понимал того, что до тех пор, пока главный поток заблокирован, другие прослушиватели событий не запускаются. Это означает, что на странице начинают проявляться «тормоза».

Как «тормозят» страницы


Вот CodePen-проект, демонстрирующий вышесказанное.


Проект, демонстрирующий ситуацию, в которой страницы «тормозят»

Нажатие на кнопку Freeze приводит к тому, что приложение начинает решать синхронную задачу. Всё это занимает 3 секунды (тут имитируется выполнение длительных вычислений). Если при этом пощёлкать по кнопке Increment — то, пока не истекут 3 секунды, значение в поле Click Count обновлено не будет. В это поле будет записано новое значение, соответствующее числу щелчков по Increment, только после того, как пройдут три секунды. Главный поток во время паузы заблокирован. В результате всё в окне приложения выглядит нерабочим. Интерфейс приложения «заморожен». События, возникающие в процессе «заморозки», ждут возможности воспользоваться ресурсами главного потока.

Если нажать на Freeze и попытаться поменять размер элемента resize me!, то, опять же, пока не истекут три секунды, размер поля не изменится. А после этого размер поля, всё же, поменяется, но при этом ни о какой «плавности» в работе интерфейса говорить не приходится.

Прослушиватели событий — это гораздо более масштабное явление, чем может показаться на первый взгляд


Любому пользователю не понравится работать с сайтом, который ведёт себя так, как показано в предыдущем примере. А ведь тут используется всего несколько прослушивателей событий. В реальном мире речь идёт совсем о других масштабах. Я решил воспользоваться в Chrome методом getEventListeners и, применив следующий скрипт, выяснить количество прослушивателей событий, прикреплённых к элементам DOM различных страниц. Этот скрипт можно запустить прямо в консоли инструментов разработчика. Вот он:

Array
  .from([document, ...document.querySelectorAll('*')])
  .reduce((accumulator, node) => {
    let listeners = getEventListeners(node);
    for (let property in listeners) {
      accumulator = accumulator + listeners[property].length
    }
    return accumulator;
  }, 0);

Я запускал этот скрипт на разных страницах и узнавал о количестве используемых на них прослушивателей событий. Результаты моего эксперимента приведены в следующей таблице.
Приложение
Количество прослушивателей событий
Dropbox
602
Google Messages
581
Reddit
692
YouTube
6054 (!!!)

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

Избавление от «тормозов» с помощью веб-воркеров


Учитывая всё вышесказанное, давайте перепишем предыдущий пример. Вот его новая версия. Выглядит она точно так же, как старая, но внутри она устроена иначе. А именно, теперь операция, которая раньше блокировала главный поток, вынесена в собственный поток. Если сделать с этим примером то же, что с предыдущим, можно заметить серьёзные позитивные отличия. А именно, если после нажатия на кнопку Freeze пощёлкать по Increment, то поле Click Count будет обновляться (после завершения работы веб-воркера, в любом случае, к значению Click Count будет добавлено число 1). То же самое касается и изменения размера элемента resize me!. Код, выполняющийся в отдельном потоке, не блокирует прослушиватели событий. Это позволяет всем элементам страницы оставаться работоспособными даже во время выполнения операции, которая раньше просто «замораживала» страницу.

Вот JS-код этого примера:

const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const count = document.getElementById('count');
const workerScript = `
  function pause(ms) {
    let time = new Date();
    while ((new Date()) - time <= ms) {}
               }

  self.onmessage = function(e) {
    pause(e.data);
    self.postMessage('Process complete!');
  }
`;
const blob = new Blob([
  workerScript,
], {type: "text/javascipt"});

const worker = new Worker(window.URL.createObjectURL(blob));

const bumpCount = () => {
  count.innerText = Number(count.innerText) + 1;
}

worker.onmessage = function(e) {
  console.log(e.data);
  bumpCount();
}

button1.addEventListener('click', async function () {
  worker.postMessage(3000);
});

button2.addEventListener('click', function () {
  bumpCount();
});

Если немного вникнуть в этот код, то можно заметить, что, хотя API Web Workers мог бы быть устроен и поудобнее, в работе с ним нет ничего особенно страшного. Вероятно, этот код выглядит страшновато из-за того, что перед вами — простой, быстро написанный демонстрационный пример. Для того чтобы повысить удобство работы с API и облегчить работу с веб-воркерами, можно воспользоваться некоторыми дополнительными инструментами. Например, мне показались интересными следующие:

  • Workerize — позволяет запускать модули в веб-воркерах.
  • Greenlet — даёт возможность выполнять произвольные фрагменты асинхронного кода в веб-воркерах.
  • Comlink — предоставляет удобный слой абстракции над API Web Workers.

Итоги


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

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

Как бы там ни было, рекомендую вам разобраться в API Web Workers. Эта технология пользуется весьма широкой поддержкой браузеров, а требования современных веб-приложений к производительности растут. Поэтому у нас нет причин отказываться от изучения инструментов, подобных веб-воркерам.

Уважаемые читатели! Пользуетесь ли вы веб-воркерами в своих проектах?





К сожалению, не доступен сервер mySQL