RequestAnimationFrame

requestAnimationFrame — это базовый механизм браузера, через который строится вся современная анимация в JavaScript. Он синхронизирует выполнение функции обновления с частотой перерисовки экрана, обеспечивая стабильные кадры без лишней нагрузки на процессор и без визуальных разрывов.

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

function loop(time) {
  console.log(time);
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Здесь time — это высокоточная метка времени в миллисекундах, синхронизированная с моментом начала навигации страницы. Она используется как основа для вычисления прогресса анимации.

Ключевая особенность такого подхода — отсутствие фиксированного интервала. Частота обновления зависит от устройства: 60Hz, 120Hz или адаптивные режимы.

Разница между requestAnimationFrame и setTimeout

Традиционные таймеры работают асинхронно и не учитывают рендеринг страницы. Это приводит к дрожанию анимации и лишним вычислениям.

requestAnimationFrame:

  • синхронизирован с рендером
  • автоматически приостанавливается в неактивных вкладках
  • снижает энергопотребление
  • обеспечивает равномерные кадры

setTimeout:

  • работает по фиксированному интервалу
  • не учитывает нагрузку браузера
  • может вызывать пропуски кадров

Именно поэтому все современные анимационные библиотеки, включая Popmotion, опираются на rAF как на базовый слой.

Кадровая модель Popmotion

Внутри Popmotion вся система анимации строится вокруг кадрового планировщика. Вместо прямого использования requestAnimationFrame библиотека использует абстракцию уровня выше — механизм синхронизации кадров.

Основная идея: отделить логику анимации от конкретного способа вызова кадрового цикла.

В упрощённом виде цикл выглядит так:

let prevTime = performance.now();

function frame(now) {
  const delta = now - prevTime;
  prevTime = now;

  updateAllAnimations(delta);
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

Каждый кадр получает delta — время между кадрами. Это значение критично для физически корректных анимаций.

Внутренний слой framesync

Popmotion использует модуль синхронизации кадров, известный как framesync. Его задача — координировать чтение и запись DOM в рамках одного кадра.

Классическая проблема браузерной анимации — layout thrashing. Она возникает, когда чтение и запись DOM чередуются хаотично:

element.style.width = "200px";
const width = element.offsetWidth;
element.style.height = width + "px";

Каждое чтение может принудительно вызывать перерасчёт layout. Framesync решает это через разделение фаз:

  • read-фаза
  • write-фаза
  • update-фаза

В рамках requestAnimationFrame это выглядит как единый цикл:

requestAnimationFrame(() => {
  readPhase();
  updatePhase();
  writePhase();
});

Дельта времени и нормализация движения

Основой всех анимаций Popmotion является корректная работа с временем. Вместо фиксированных шагов используется delta time.

function update(delta) {
  position += velocity * delta;
}

Проблема прямого использования delta заключается в нестабильности кадров. Поэтому Popmotion нормализует время относительно 16.667 мс (идеальный кадр при 60Hz):

const normalized = delta / 16.667;

Это позволяет анимации сохранять одинаковую скорость на разных устройствах.

Связь requestAnimationFrame и spring-анимаций

Физические анимации Popmotion (spring) зависят от непрерывного обновления состояния системы. Каждый кадр представляет собой шаг численного интегрирования.

Упрощённая модель spring:

function springStep(state, delta) {
  const stiffness = 0.1;
  const damping = 0.8;

  const force = -stiffness * state.position;
  state.velocity += force * delta;
  state.velocity *= damping;
  state.position += state.velocity * delta;

  return state;
}

requestAnimationFrame обеспечивает равномерные шаги времени, без чего физическая модель теряет стабильность и начинает «взрываться» или затухать некорректно.

Поток обновления анимаций Popmotion

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

  1. Получение времени кадра через requestAnimationFrame
  2. Вычисление delta
  3. Обновление всех активных анимаций
  4. Вычисление новых значений свойств
  5. Запись значений в DOM или стилевые свойства

Схематично:

requestAnimationFrame(step);

function step(now) {
  const delta = now - last;
  last = now;

  animations.forEach(anim => anim.update(delta));
  animations.forEach(anim => anim.render());

  requestAnimationFrame(step);
}

Оптимизация через группировку обновлений

Popmotion минимизирует количество вызовов DOM за счёт группировки изменений. Вместо того чтобы обновлять каждый объект отдельно, изменения собираются в очередь.

const writeQueue = [];

function scheduleWrite(fn) {
  writeQueue.push(fn);
}

function flushWrites() {
  for (let i = 0; i < writeQueue.length; i++) {
    writeQueue[i]();
  }
  writeQueue.length = 0;
}

flushWrites вызывается строго в рамках одного кадра requestAnimationFrame, что гарантирует консистентное состояние DOM.

Высокочастотные экраны и адаптация цикла

Современные дисплеи могут работать на 120Hz и выше. requestAnimationFrame автоматически подстраивается под частоту обновления экрана.

Это означает:

  • на 60Hz кадр ~16.67ms
  • на 120Hz кадр ~8.33ms

Popmotion не фиксирует частоту, а работает исключительно через delta time, что делает поведение стабильным на любых устройствах.

Приостановка и экономия ресурсов

Одно из скрытых преимуществ requestAnimationFrame — автоматическая деактивация в неактивных вкладках. Браузер перестаёт вызывать callback, снижая нагрузку.

Popmotion использует это поведение для:

  • остановки анимаций без дополнительной логики
  • предотвращения утечек времени
  • синхронизации состояния при возврате вкладки

Координация нескольких анимаций в одном кадре

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

Popmotion объединяет их в единый тик:

function globalTick(now) {
  const delta = now - last;
  last = now;

  for (let anim of animations) {
    anim.step(delta);
  }
}

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

requestAnimationFrame как основа детерминизма

Несмотря на асинхронную природу браузера, rAF создаёт детерминированный поток обновлений. Каждый кадр становится атомарной единицей времени, внутри которой выполняются все изменения состояния.

Popmotion строит свою модель именно на этом свойстве: кадр как единица вычисления, а не произвольный callback таймера.