Проверьте свое понимание, если вы эксперт

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

Анимация - это не единственное и не главное предназначение requestAnimationFrame(). Название этого метода вводит в заблуждение. requestAnimationFrame() также незаменим для рендеринга сложных пользовательских интерфейсов, не тратя время на принудительные макеты.

Ниже я использую минимальные образцы кода, чтобы подробно объяснить, как работает метод. Читая их, вы можете проверить свое понимание основ веб-разработки. Думаю, только молодой разработчик может ответить на три основных вопроса, приведенных ниже.

Простой вопрос

Начну с простого вопроса. Представьте себе страницу, содержащую один div:

<div id="root"></div>

Страница загружает скрипт, который вставляет ту же строку Timeout в div:

// 1.js
function timeout() {
    root.innerHTML='Timeout'
    setTimeout(timeout);
}
timeout();

Что увидят пользователи при запуске кода? Сложно дать неправильный ответ, но если вы не уверены, то можете увидеть ответ здесь.

Более сложный вопрос

Что увидят пользователи, если на той же странице с одним div будет загружен более простой код вместо приведенного выше скрипта:

// 2.js
while (true){
    root.innerHTML='Timeout'
}

Вы можете открыть страницу с кодом, но вы ничего не увидите. Это иллюстрация важного факта - один и тот же поток выполняет JavaScript и отображает DOM. Браузер не может выполнять JavaScript и рисовать одновременно. Чтобы пользователи могли видеть какие-либо изменения в DOM, в выполнении кода JavaScript должны быть перерывы. В первом примере вы можете увидеть сообщение, вставленное в DOM, потому что браузеру предоставляется возможность рисовать между обратными вызовами, вставленными в очередь задач с помощью setTimeout(). Фактически, код планирования для выполнения после задержки является вторичной функцией setTimeout(). Основная функция setTimeout() - добавление задач в очередь, общую с другими задачами, чтобы браузер оставался отзывчивым.

Прежде чем задать последний заключительный вопрос, я должен упомянуть другие важные свойства setTimeout() и requestAnimationFrame().

Частота обновления - 60 кадров в секунду.

Браузер перерисовывает DOM на экране пользователя со скоростью не выше 60 кадров в секунду и только в том случае, если есть что перерисовывать. Это означает, что requestAnimationFrame() обратные вызовы выполняются не чаще 60 раз в секунду или, другими словами, не чаще, чем каждые 1/60 = 17 мс. Следующий код вычисляет и отображает в консоли время между последовательными requestAnimationFrame() выполнениями обратного вызова:

// 3.js
let previous = {};
function log(method) {
    const now = Date.now();
    console.log(now - previous[method]);
    previous[method] = now;
}
function frame() {
    log("frame");
    requestAnimationFrame(frame);
}
frame();

Если открыть страницу, в консоли можно увидеть, что requestAnimationFrame() обратные вызовы действительно не вызываются чаще, каждые 16–17 мс:

Если консоль открыта в отдельном окне, вы также заметите, что requestAnimationFrame() обратные вызовы не выполняются, если страница не отображается на экране компьютера.

Частота выполнения setTimeout() обратных вызовов

Если в приведенном выше коде я заменяю frame() новой функцией:

// 4.js
function timeout() {
    log("timeout");
    setTimeout(timeout);
}
timeout();

В консоли кажется, что обратные вызовы setTimeout() выполняются чаще, чем обратные вызовы requestAnimationFrame() - примерно каждые 7-9 мс:

Гонка между setTimeout() и requestAnimationFrame()

Чтобы убедиться, что обратные вызовы setTimeout() выполняются чаще, чем обратные вызовы requestAnimationFrame(), я объединил код из приведенных выше сценариев 3.js и 4.js и организовал гонку между обратные вызовы двух методов:

// 5.js
let previous = {};
function log(method) {
    const now = Date.now();
    console.log(method,now - previous[method]);
    previous[method] = now;
}
function frame() {
    log("frame");
    requestAnimationFrame(frame);
}
function timeout() {
    log("timeout");
    setTimeout(timeout);
}
timeout();
frame();

В консоли видно, что действительно до трех setTimeout() callback-ов умещается между двумя перерисовками при максимальной скорости 60 fps.

Сложный вопрос

Чтобы задать последний и заключительный вопрос, я усложнил код самого первого примера 1.js:

function frame() {
    root.innerHTML='Frame'
    requestAnimationFrame(frame);
}
function timeout() {
    root.innerHTML='Timeout'
    setTimeout(timeout);
}
timeout();
frame();

Обратные вызовы как setTimeout(), так и requestAnimationFrame() продолжают изменять содержимое div на id root. Выше вы видели, что обратный вызов setTimeout() изменяет сообщение div на Тайм-аут до трех раз чаще, обратный вызов requestAnimationFrame() меняет сообщение на Frame.

Как вы думаете, что пользователи видят на экране - тайм-аут или фрейм?

Выводы

Итак, вы видите, что изменения в DOM, внесенные до requestAnimationFrame() обратного вызова, могут не иметь значения, потому что requestAnimationFrame() обратный вызов - это последний код, который выполняется перед тем, как браузер преобразует DOM в изображение на экране. Это также означает, что обратный вызов вызывается, когда предыдущий кадр полностью завершен - обратный вызов может быстро, без синхронных перекомпоновок, считывать размеры и координаты всех элементов страницы.

Предотвращение принудительного переполнения - основной вариант использования requestAnimationFrame() в веб-приложениях. Типичный пример из моей работы - отображение огромной формы, в которой видно все содержимое каждого textarea. Если я регулирую высоту текстового поля до того, как оно было нарисовано, вся страница будет нарисована через несколько секунд из-за несвоевременных расчетов макета.

Я также использую requestAnimationFrame() для выбора оптимальных способов визуализации данных. В своих предыдущих постах я продемонстрировал отсутствие ощутимой разницы между методами, предназначенными для манипуляции с DOM. До этого я добился значительного сокращения времени рендеринга и, таким образом, увеличил время за ту же зарплату, используя свойство content-visibility CSS.

Код примеров можно скачать с страницы примеров или Github.