Использование requestAnimationFrame

Lottie Web использует requestAnimationFrame как базовый механизм синхронизации отрисовки с циклом обновления браузера. Это ключевой элемент, определяющий плавность анимаций, контроль FPS и поведение при изменении видимости вкладки.

requestAnimationFrame (rAF) запускает функцию рендера перед следующим перерисовыванием экрана, обычно с частотой 60 кадров в секунду, но адаптивно снижая нагрузку при необходимости. В контексте Lottie это означает, что каждый кадр анимации вычисляется и отрисовывается строго в ритме браузера.


Как Lottie Web использует requestAnimationFrame

Внутренний цикл анимации в Lottie строится вокруг постоянного обновления текущего времени анимации и пересчёта состояния слоёв:

  • вычисление прогресса анимации
  • интерполяция ключевых кадров
  • обновление transform-матриц
  • перерисовка SVG / Canvas / HTML5 renderer

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

function tick() {
  const currentTime = performance.now();

  updateAnimationState(currentTime);
  renderFrame();

  requestAnimationFrame(tick);
}

requestAnimationFrame(tick);

Каждый вызов tick синхронизирован с refresh rate дисплея, что исключает разрывы кадров и лишние перерасчёты.


Привязка времени к animationData

Lottie не «играет» анимацию по кадрам напрямую. Вместо этого используется временная шкала:

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

Формула внутреннего расчёта:

progress = (currentTime - startTime) / duration;
frame = progress * totalFrames;

Далее frame округляется или интерполируется в зависимости от режима smooth playback.


Роль requestAnimationFrame в управлении FPS

Использование rAF даёт Lottie несколько критичных преимуществ:

  • синхронизация с GPU-композицией браузера
  • автоматическое ограничение FPS на фоне
  • снижение нагрузки при скрытой вкладке
  • отсутствие необходимости ручного setInterval

При этом Lottie не пытается «форсировать» FPS выше возможностей устройства. Если экран обновляется на 144 Hz, рендер будет соответствовать этому значению.


Разница между play() и кастомным rAF-циклом

Стандартный вызов:

const anim = lottie.loadAnimation({
  container: el,
  renderer: 'svg',
  loop: true,
  autoplay: true,
  path: 'data.json'
});

В этом случае Lottie самостоятельно создаёт внутренний rAF-цикл.

При кастомном управлении анимацией автоматический цикл может быть отключён:

const anim = lottie.loadAnimation({
  container: el,
  renderer: 'svg',
  loop: false,
  autoplay: false,
  path: 'data.json'
});

Далее управление берётся вручную:

let start;

function loop(timestamp) {
  if (!start) start = timestamp;

  const elapsed = timestamp - start;
  const frame = (elapsed / 1000) * 60;

  anim.goToAndStop(frame, true);

  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Такой подход применяется в сценариях:

  • синхронизация с scroll
  • управление через WebGL сцену
  • аудиореактивные анимации
  • кастомные таймлайны

requestAnimationFrame и scroll-driven анимации

Одна из типичных задач — привязка Lottie к прокрутке страницы. rAF используется как буфер между scroll-событиями и отрисовкой, чтобы избежать избыточных пересчётов.

Архитектура выглядит так:

  • scroll обновляет переменную progress
  • rAF считывает актуальное значение progress
  • Lottie обновляет кадр
let scrollProgress = 0;

window.addEventListener('scroll', () => {
  const max = document.body.scrollHeight - window.innerHeight;
  scrollProgress = window.scrollY / max;
});

function render() {
  const frame = scrollProgress * anim.totalFrames;

  anim.goToAndStop(frame, true);

  requestAnimationFrame(render);
}

requestAnimationFrame(render);

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


Оптимизация: избегание лишних перерисовок

При работе через rAF важно исключать ситуации, когда Lottie обновляется без необходимости:

  • одинаковый frame не должен перерисовываться
  • не следует вызывать render при отсутствии изменений
  • лучше кэшировать последний frame

Пример оптимизации:

let lastFrame = -1;

function render() {
  const frame = Math.floor(scrollProgress * anim.totalFrames);

  if (frame !== lastFrame) {
    anim.goToAndStop(frame, true);
    lastFrame = frame;
  }

  requestAnimationFrame(render);
}

Это снижает нагрузку на DOM и SVG-движок.


Поведение requestAnimationFrame в фоне

Браузеры автоматически приостанавливают или замедляют rAF:

  • вкладка неактивна → FPS падает до 1–2 или останавливается
  • минимизация окна → цикл замораживается
  • возврат фокуса → цикл восстанавливается

Lottie учитывает это поведение, но при кастомных циклах требуется ручная защита:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    anim.pause();
  } else {
    anim.play();
  }
});

Синхронизация нескольких Lottie-анимаций

При работе с несколькими экземплярами важно избегать создания отдельных rAF-циклов для каждого:

Антипаттерн:

  • каждый анимированный объект запускает свой requestAnimationFrame

Оптимальный подход:

  • один глобальный rAF
  • централизованный update всех анимаций
const animations = [];

function globalLoop() {
  animations.forEach(anim => {
    anim.update();
  });

  requestAnimationFrame(globalLoop);
}

requestAnimationFrame(globalLoop);

Это особенно важно при большом количестве SVG-анимаций.


Интеграция с производительным рендерингом

При использовании Canvas или WebGL renderer Lottie всё равно опирается на rAF, но:

  • SVG → DOM-операции
  • Canvas → pixel buffer update
  • WebGL → GPU draw calls

requestAnimationFrame остаётся точкой синхронизации вне зависимости от backend-рендера, обеспечивая единый таймлайн обновления.


Типичные ошибки при использовании rAF с Lottie

  • запуск нескольких независимых циклов без контроля
  • отсутствие остановки анимации при unmount компонента
  • попытка управлять кадрами через setInterval
  • двойной autoplay (внутренний Lottie + внешний rAF)
  • отсутствие проверки visibilityState

Каждая из этих проблем приводит к утечкам CPU и деградации FPS.


Тайминг и drift при длительных анимациях

При длинных анимациях (десятки секунд и более) важно учитывать дрейф времени. Использование timestamp из rAF предпочтительнее накопительного счётчика:

let start;

function loop(time) {
  if (!start) start = time;

  const elapsed = time - start;
  const frame = (elapsed / anim.frameRate) * 0.001;

  anim.goToAndStop(frame, true);

  requestAnimationFrame(loop);
}

Такой подход компенсирует микрозадержки и сохраняет синхронизацию.


Итоговая модель взаимодействия

requestAnimationFrame выступает центральным координатором:

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

Lottie Web опирается на него как на базовый «таймер кадра», превращая JSON-анимацию в поток кадров, строго привязанный к циклу отрисовки браузера.