Learning Book

requestAnimationFrame и requestIdleCallback

requestAnimationFrame и requestIdleCallback

requestAnimationFrame (rAF)

requestAnimationFrame запускает callback перед следующей перерисовкой браузера:

// Анимация через rAF
function animate(timestamp) {
  // timestamp — время в мс от начала документа
  const progress = (timestamp - startTime) / duration

  if (progress < 1) {
    element.style.transform = `translateX(${progress * 200}px)`
    requestAnimationFrame(animate)  // следующий кадр
  } else {
    element.style.transform = 'translateX(200px)'  // финальное значение
  }
}

const startTime = performance.now()
const duration = 1000  // 1 секунда
requestAnimationFrame(animate)
rAF вызывается 60 раз в секунду (каждые ~16.6 мс), синхронизируясь с частотой обновления экрана. Если вкладка неактивна — rAF не вызывается (экономит батарею).

Отмена анимации

let animationId = null

function startAnimation() {
  const start = performance.now()

  function frame(timestamp) {
    const elapsed = timestamp - start
    element.style.opacity = Math.min(elapsed / 500, 1)

    if (elapsed < 500) {
      animationId = requestAnimationFrame(frame)
    }
  }

  animationId = requestAnimationFrame(frame)
}

function stopAnimation() {
  if (animationId !== null) {
    cancelAnimationFrame(animationId)
    animationId = null
  }
}

requestIdleCallback (rIC)

requestIdleCallback запускает задачи во время простоя браузера:

// Выполняем не срочную работу, когда браузер свободен
requestIdleCallback((deadline) => {
  console.log('Оставшееся время:', deadline.timeRemaining(), 'мс')
  console.log('Таймаут истёк:', deadline.didTimeout)

  // Обрабатываем элементы пока есть время
  while (deadline.timeRemaining() > 0 && items.length > 0) {
    processItem(items.shift())
  }

  // Если не закончили — продолжим в следующем idle
  if (items.length > 0) {
    requestIdleCallback(processQueue)
  }
}, { timeout: 2000 })  // выполнить максимум через 2 сек (даже если браузер занят)

Когда использовать rAF vs rIC

// requestAnimationFrame — для визуальных обновлений:
// - CSS анимации через JS
// - Обновление canvas
// - Любые изменения DOM, которые должны выглядеть плавно
requestAnimationFrame(() => {
  element.style.transform = `translateX(${x}px)`
})

// requestIdleCallback — для фоновых задач:
// - Предзагрузка данных
// - Аналитика
// - Сохранение в localStorage
// - Не срочная обработка данных
requestIdleCallback(() => {
  analytics.send('page_viewed')
  localStorage.setItem('draft', JSON.stringify(data))
})

requestIdleCallback не поддерживается в Safari (до iOS 16.4). Простая эмуляция:

// Полифил для rIC
window.requestIdleCallback = window.requestIdleCallback || function(fn, options) {
  const start = Date.now()
  return setTimeout(function() {
    fn({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50 - (Date.now() - start))
      }
    })
  }, 1)
}

Настоящий rIC учитывает реальное состояние браузера. Полифил через setTimeout даёт 50 мс окна — достаточно для большинства задач.