Вот пять антишаблонов для таймеров в JavaScript и способы их правильного применения.

У современной веб-разработки есть много проблем, связанных с асинхронными аспектами. В JavaScript мы склонны обрабатывать некоторые из них с помощью таймеров. Под таймерами я подразумеваю setTimeout и setInterval. Однако в большинстве случаев это не очень хорошая идея, поскольку таймеры неэффективны и могут вести себя не так, как нам хотелось бы. В этой статье я объясню некоторые особенности поведения таймеров и опишу сценарии, в которых таймеры являются антипаттернами, и какие решения следует предпочесть.

В целом можно сказать, что таймеры используются для опроса информации. Опрос — это одна из двух концепций программирования для получения новых данных. Используя опрос, мы постоянно спрашиваем источник данных, есть ли обновление для определенного значения. С другой стороны, концепция толчка сообщит нам, когда что-то, что нам небезразлично, изменилось, и тогда мы сможем отреагировать на это. Если вы не знакомы с этими методами, вы можете представить отправку как событие, которое мы можем прослушивать, в то время как опрос — это цикл, который возвращает значение только при изменении источника данных. Push обычно предпочтительнее опроса.

Цепные таймеры

В JavaScript можно связать таймеры. Понимание концепции цепочки важно при использовании таймеров. Взгляните на следующий пример:

let i = 0;
setInterval(() => console.log(++i), 100);

Это зарегистрирует 1 2 3 4 5 … и так далее. Вывод будет остаться в порядке, потому что выполнение функции, объявленной в нашем таймере, связано в правильном порядке, и мы можем полагаться на то, что цепочка выполняется одно за другим. Хотя мы не знаем времени между двумя казнями. Мы знаем только, что это не менее 100 мс. Но, как мы скоро узнаем, приведенный выше пример может занять даже пять минут (!) до его выполнения — даже если мы больше ничего не делаем в нашем коде (например, даже если мы не связываем длительную операцию к таймерам).

То же самое верно и для setTimeout:

let i = 0;
const myTimer = () => {
  setTimeout(() => {
    console.log(++i);
    myTimer(); // <- chain another timer
  }, 100)
}
myTimer();

Это создаст тот же результат.

Таймеры JavaScript и производительность

Таймеры будут будить ЦП каждый раз, когда они запускаются. setTimeout и setInterval по своей сути не требуют интенсивного использования ЦП, но они запускают функции, которые потенциально являются такими. Таким образом, использование таймеров может быть плохой идеей, в зависимости от того, как часто вы хотите, чтобы ваши таймеры запускались. Как правило, запускать таймеры несколько раз в секунду — плохая идея, особенно на недорогих мобильных устройствах. Кроме того, не полагайтесь на то, что ваши таймеры сработают вовремя. Вы не можете полагаться на то, что ваши таймеры будут выполняться с заданным интервалом, особенно когда ваши пользователи отключаются. Браузеры будут регулировать таймеры для экономии энергии.

Таймеры JavaScript и регулирование

Следующее может обрабатываться по-разному для каждого браузера. Однако в этой главе мы будем рассматривать только Chrome. Различия должны быть незначительными и могут в основном относиться к определению видимой страницы. Как правило, скрытая страница — это страница, которая свернута или не находится на активной вкладке. Но браузеры могут решить, что страница также скрыта, когда ее содержимое полностью невидимо и т. д. Вы можете использовать Page Visibility API, чтобы проверить, когда срабатывает изменение видимости (https://developer.mozilla.org/en-US/docs /Web/API/Page_Visibility_API).

Chrome решает, когда и как регулировать таймеры на разных этапах:

Этап 1. Минимальное регулирование

Минимальное регулирование применяется, когда выполняется одно из следующих условий:

  • Страница видна.
  • Страница «наделала шума» за последние 30 секунд. В Chrome ваши вкладки будут отображать значок звука рядом с заголовком, когда это произойдет. Приглушенный звук не считается.

Таймер не регулируется, если тайм-аут не меньше 4 мс и 100 или более таймеров соединены в цепочку. В этом случае время ожидания установлено на 4 мс.

Примечание. Ограничение в 100 связанных таймеров в Chrome является новым. Раньше было всего пять часов.

Шаг 2. Среднее регулирование

Браузеры делают это, когда минимальное регулирование не применяется и выполняется какое-либо из следующих условий:

  • Количество цепочек меньше 100.
  • Страница была скрыта менее пяти минут.
  • Страница использует WebRTC. Это означает, что есть либо открытый RTCPeerConnection, либо живой MediaStreamTrack.

Затем таймеры в цепочке будут проверяться один раз в секунду. Из-за этого могут быть перекрывающиеся таймеры из разных цепочек. Эти функции будут объединены в пакеты, а затем выполнены.

Шаг 3. Интенсивное регулирование

Если не применяется ни минимальное, ни среднее регулирование и всеиз следующих условий выполняются, то будет применяться интенсивное регулирование:

  • Страница была скрыта более 5 минут.
  • Количество цепочек больше 100.
  • Страница не воспроизводит звук в течение как минимум 30 секунд.
  • WebRTC не используется.

В этом случае Браузер будет проверять таймеры раз в минуту. Перекрывающиеся таймеры из разных групп будут выполняться в пакетном режиме.

Распространенные анти-шаблоны с таймерами в JavaScript

Теперь, когда мы кое-что прояснили о таймерах и о том, как их можно регулировать, давайте рассмотрим некоторые антипаттерны и способы их избежать.

Проверка наличия элемента в области просмотра

Вы можете использовать IntersectionObserver для обнаружения элементов в текущем окне просмотра (https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). Вместо опроса это будет инициированное событие, и вы сможете на него отреагировать.

Общие варианты использования IntersectionObservers над таймерами:

  • Ленивая загрузка контента по мере прокрутки страницы (также учитывайте атрибут загрузки изображений).
  • Запуск задач, когда элемент появляется в поле зрения (например, применение стилей)

В статье выше упоминаются другие варианты использования, но вы, вероятно, не использовали бы таймеры для решения этих проблем раньше.

Проверка изменения размера элемента

Как и в предыдущем примере, мы можем использовать ResizeObserver для проверки того, изменились ли размеры элемента (https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).

Общие варианты использования ResizeObservers:

  • Реализуйте медиа-запросы для каждого элемента, чтобы императивно определить точки останова и стили.
  • Сохраняйте позицию прокрутки: если вы не хотите сбивать пользователей с толку и сохранить их текущую позицию прокрутки в списке, даже если стили элементов меняются (из-за медиа-запросов), вы можете использовать ResizeObserver для настройки позиции прокрутки.

Проверка того, не изменился ли DOM (элементы добавлены/удалены)

Это один антипаттерн, который я все еще вижу время от времени. Следует избегать использования таймеров для проверки наличия элемента в DOM.

Вместо этого вы можете использовать MutationObserver (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) в качестве альтернативы push. Если вы имеете дело с пользовательским элементом, вы можете использовать события жизненного цикла для пользовательских элементов (https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks ).

Общие варианты использования MutationObservers:

  • Проверьте, был ли элемент добавлен или удален из DOM.

Однако будьте осторожны с этим! Многие библиотеки, такие как React, могут решить эту проблему с помощью своих инструментов. Итак, если вы не используете ванильный JavaScript, вам может не понадобиться MutationObserver.

Наблюдение за состоянием в целом

Не используйте setInterval или setTimeout для наблюдения за состоянием. Например, не используйте setInterval для проверки того, было ли изменено значение переменной. Есть лучшие способы решения подобных проблем. Например:

Анимация в целом

Анимации не должны без необходимости будить ЦП. Поэтому вы не должны запускать анимацию с помощью таймеров. Лучше создавайте анимацию, используя любой из них:

И CSS-анимация, и Animations API гарантируют, что анимация вычисляется только для кадров, которые может отображать устройство. Используя таймеры в JS, вы можете вычислять функции в промежуточных кадрах, которые устройство не может отрисовать.

Помимо этого, анимации, которые вы объявляете заранее с помощью API, могут быть дополнительно оптимизированы браузером. Например, автоматический композитинг.

Вы все еще можете использовать таймеры, если ваша анимация имеет очень низкую частоту кадров. Чтобы гарантировать, что вы рисуете только кадры, которые устройство может отобразить, вам все равно следует комбинировать решение таймеров с requestAnimationFrame.

Запрос информации с сервера

Вместо опроса информации с сервера (например, выборки в setInterval для проверки новых данных) вы можете использовать один из следующих способов (порядок произвольный):

Оригинальная статья и идея пришли от Джейка Арчибальда, и изначально она была написана здесь: https://developer.chrome.com/blog/timer-throttling-in-chrome-88/

Я обновил некоторую информацию, так как статья уже несколько старше. Обратите внимание, что в статье упоминается ограничение в пять цепочек для запуска дросселирования. Это больше не относится к Chrome (со 2 августа 2022 года, Chrome v104).

Вот и все, народ!

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

Большое спасибо!

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord.