Путешествие по JavaScript таймерам в сети

Stas Bagretsov
8 min readSep 13, 2018

--

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

Перевод статьи A tour of JavaScript timers on the web

Быстрый опрос: в чем разница между этими JavaScript таймерами?

Promises

setTimeout

setInterval

setImmediate

requestAnimationFrame

requestIdleCallback

А конкретнее, если мы выставим в очередь все эти таймеры за раз, то будет ли у вас представление о том, в каком порядке они сработают?

Если нет, то вы не один. Я работал с JavaScript и веб программированием годами, я работал на компанию разработчика браузера два года и только недавно я пришел к пониманию того, как работают эти таймеры и как они срабатывают вместе.

В этом посте я дам вам высококлассный обзор их работы и когда их нужно использовать. Я также расскажу про функции из Lodash — debounce() и throttle(), так как тоже нахожу их довольно полезными.

👉Мой Твиттер — там много из мира фронтенда, да и вообще поговорим🖖. Подписывайтесь, будет интересно: ) ✈️

Промисы и микротаски

Давайте сразу с этим разберемся, так как возможно это самое простое из того, что будет. Колбэк промиса также известен как “микротаск” и он работает в такой же частотности, как и MutationObserver колбэки.

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

Для примера, скажем, что у нас есть функция которая блокирует основной поток на одну секунду:

function block() {var start = Date.now()while (Date.now() — start < 1000) { /* wheee */ }}Если бы мы выставили в очередь несколько микротасков для вызова этой функции:for (var i = 0; i < 100; i++) {Promise.resolve().then(block)}

Это бы заблокировало браузер примерно на 100 секунд. Буквально это то же самое, если бы мы сделали так:

for (var i = 0; i < 100; i++) {block()}

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

setTimeout and setInterval

Это два братца: setTimeout ставит таск в очередь на несколько миллисекунд, в то время как setInterval откладывает выполнение повторяющегося таска на указанное количество тех же миллисекунд.

Суть в том, что браузеры по факту не соблюдают эти миллисекунды. Исторически веб-разработчики злоупотребляли setTimeout. Очень много. До такой степени, что браузерам пришлось добавить поблажки для setTimeout(/* … */, 0), чтобы избежать блокировки основного потока, так как кучи сайтов склонны разбрасываться setTimeout(0) как конфетти.

Это причина, по которой многие фичи в crashmybrowser.com больше не работают, например выстраивание в очередь setTimeout, который вызывает более двух других setTimeout, которые вызывают еще больше двух других setTimeout и так далее. Тут рассказано про это с точки зрения Edge.

Проще говоря, setTimeout(0) на самом деле не запускается в ноль миллисекунд. Обычно это происходит в 4. А иногда и в 16 (так делает Edge, когда девайс работает от аккумулятора). Иногда это может быть сжато до одной секунды. Есть несколько трюков, которые придумали браузеры, чтобы предотвратить то, когда веб-страница сжирает ваш CPU, выполняя бесполезную setTimeout работу.

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

setImmediate

Перед тем как двигаться дальше к API получше, стоит упомянуть кое-что ещё, а именно setImmediate, который как бы это сказать за отсутствием слова получше — странный. Если вы поищите его на caniuse.com, то увидите, что только браузеры от Microsoft могут его поддерживать. И пока что это есть в Node.js и имеет множество полифиллов в npm. Но что это такое?

setImmediate был изначально предложен Microsoft, чтобы охватить проблемы с setTimeout, описанные выше. Попросту, setTimeout использовали сильно злоупотребляя и таким образом суть была в том, чтобы создать что-то новое, что будет setImmediate(0) и на самом деле будет работать как setImmediate(0) без качелей в 4 миллисекунды.

К сожалению, setImmediate был принят только в IE и Edge. Отчасти это до сих пор используется, потому что имеет некую суперсилу в IE, где он позволяет вставлять события с клавиатуры и клика мышки, чтобы перепрыгнуть всю очередь и сработать перед выполнением колбэка в setImmediate, тогда как IE не может похвастаться таким волшебством с setTimeout.

Так же, факт того, что setImmediate существует в Node, означает то, что много node-полифилл кода используется в браузере без реального понимания того, что он делает. Он не помогает в том, что разницы между setImmediate и process.nextTick в Node довольно запутанны и даже официальная документация Node говорит о том, что названия должны быть поменяны местами.

Суть в том, чтобы использовать setImmediate, если вы знаете, что вы делаете и пытаетесь оптимизировать работу с вводом в IE. Если же нет, то даже не заморачивайтесь или только используйте его в Node.

requestAnimationFrame

И так, теперь мы подошли к самому важному заменителю setTimeout, таймеру который цепляется к циклу рендера браузера. Кстати, если вы не знаете того, как работает цикл событий в браузере, то я вам очень рекомендую это видео от Джейка Арчибальда. Посмотрите, мы подождем.

И так, requestAnimationFrame на самом деле работает так: это, как и setTimeout, только вместо ожидания неясного количества времени (4ms, 16ms или секунды), оно выполняется перед следующим шагом стилевых и шаблонных вычислений. Как указывалось в видео выше, нет ничего плохого в том, что выполнение происходит после этого шага в Safari, IE и Edge < 18, но давайте пока забудем про это, так как это обычно совершенно неважная деталь.

Вот, что я думаю о requestAnimationFrame: всякий раз, когда я хочу проделать какую-либо работу, которая будет модифицировать стиль или шаблон — к примеру, изменение CSS свойств или запуск анимации — Я прикреплю это к requestAnimationFrame, далее rAF. Это гарантирует мне вот что:

У меня с меньшей долей вероятности поедет шаблон, так как все изменения в DOM выстраиваются в очередь и координируются.

Мой код естественным образом адаптируется к характеристикам производительности браузера. Для примера, если это дешевое устройство, которое испытывает проблемы с рендерингом элементов, rAF замедлится исходя из обычных интервалов в 16.7ms (На экранах 60 Hertz) и, следовательно устройство не заглючит так, как это могло бы случиться с setTimeout и setInterval.

Вот почему библиотеки анимаций, которые не полагаются на CSS переходы или keyframes, такие, как GreenSock или React Motion, будут обычно делать изменения в колбэке rAF. Если вы анимируете элемент между opacity: 0 и opacity: 1, то нет смысла в выстраивании миллиона колбэков и анимирования всех возможных промежуточных состояний, включая opacity: 0.0000001 и opacity: 0.9999999.

Вместо этого, вы бы лучше использовали rAF, чтобы сказать браузеру, то сколько фреймов вы можете отрисовать за заданный период времени и вычислить “анимацию” для этого времени. Таким образом, медленные устройства окончат выполнение с медленным фреймрейтом, а быстрые с быстрым, что было бы не совсем правдой, используйте вы что-то вроде setTimeout, который работает независимо от скорости рендеринга браузера.

requestIdleCallback

rAF это возможно самый полезный таймер в этом наборе, но requestIdleCallback стоит упоминания. Поддержка браузерами тут не очень хороша, но есть полифиллы, которые работают вполне отлично, используя rAF в своей основе.

Во многих случаях rAF очень схож с requestIdleCallback. Далее я буду его называть rIC.

Как и rAF, rIC естественным образом будет подстраиваться под характеристики производительности браузера: если устройство под большой нагрузкой, то rIC может выполниться с задержкой. Разница тут в том, что rIC запускает во время “ленивого” состояния браузера, то есть тогда, когда браузер осознает, что у него нет каких-либо тасков, микротасков или событий ввода которые требуется обработать и тогда вы можете делать свою работу. Также это дает вам некий “дедлайн”, чтобы отслеживать как вы используете свои ресурсы, что кстати есть вполне хорошая фича.

У Дэна Абрамова есть хорошая речь на JSConf Iceland 2018, где он показывает как можно было бы использовать rIC. В этой речи, у него веб-приложение, которое вызывает rIC для каждого клавиатурного события, в то время как пользователь печатает и затем обновляет отрендеренное состояние внутри колбэка. Это отлично, так как быстрая печать может вызывать множество keydown/keyup событий, которые будут быстро запускаться, но по факту вам необязательно нужно обновлять отрендеренное состояние страницы при каждом нажатии на клавишу.

Ещё один хороший пример для этого — это “показатель оставшихся символов” как в Twitter или в Mastodon. Я использую rIC в Pinafore, так как мне не очень важно будет ли индикатор обновляться при каждом нажатии. Если я пишу быстро, то лучше приоритизировать отклик на ввод, так чтобы не выбиваться из ритма.

Кое-что, что я подметил по поводу rIC, это то, что он немного привередливый в Chrome. В Firefox срабатывает тогда, когда я захочу, интуитивно понимая, что браузер в “ленивом” состоянии и готовясь запустить код. В мобильном Chrome для Android, я подметил то, что всегда, когда я скроллю тачем, то может быть задержка с rIC на несколько секунд, даже после того как я перестал прикасаться к экрану и браузер перестал вообще что-то делать.

В любом случае, rIC это ещё один отличный инструмент, чтобы добавить его в свой инструментарий. Я склонен полагать, что rAF нужно использовать для важного рендеринга, а rIC нет.

debounce and throttle

Есть две функции, которые не встроены в браузер, но они настолько полезны, что они стоят своего упоминания. Если вы с ними не знакомы, то вот тут есть их разбор на CSS Tricks.

Обычно я использую debounce в пределах resize колбэка. Когда пользователь ресайзит браузер, то нет смысла обновлять шаблон при каждом resize колбэке, так как он срабатывает слишком часто. Вместо этого, вы можете использовать debounce на несколько сотен миллисекунд, чтобы удостовериться в том, что колбек в конечном итоге сработал, после того как пользователь завершить вращать окно браузера как гармошку.

throttle это то, что я использую более обильно. Для примера, хорошо его использовать вместе с событием scroll. И снова, обычно бессмысленно пытаться обновлять отрендеренное состояние приложения при каждом scroll колбэке, так как он срабатывает слишком часто, а частотность может варьироваться от браузера к браузеру и от метода ввода к методу ввода. В общем понятно.

Использование throttle нормализует такое поведение и дает удостовериться, что оно срабатывает каждые X миллисекунд. Вы также можете настроить throttle или debounce в Lodash, чтобы они срабатывали при начале задержки, в её конце или в обоих случаях.

Для контраста, я бы не стал использовать debounce для скроллинга, так как я не хочу, чтобы UI обновлялся после того, как пользователь перестанет скроллить. Это может раздражать или даже запутывать, так как пользователь может разочароваться и попытаться продолжать скроллить с целью обновить состояние UI. В этом случае throttle лучше, так как он не ждет scroll события, чтобы прекратить выполнение.

throttle это функция, которую я использую повсеместно во всех вводах и даже для некоторых регулярных задач, таких как очистка БД. Это очень полезно. Может когда-нибудь её и включат в браузеры.

Завершение

И так, это мой шальной тур по разным таймерам, доступным в браузерах и тому, как вы можете их использовать. Возможно, я некоторые пропустил, так как есть уж определенно экзотические, такие как postMessage или событийный цикл. Но я надеюсь, что это хотя бы дает хорошее представление о том, как я вижу JavaScript таймеры в сети.

--

--

Stas Bagretsov

Надеюсь верую вовеки не придет ко мне позорное благоразумие. webdev/sports/books