Frontender Magazine

Запрашивайте кадры анимации для лучшей производительности

Есть несколько известных способов работать с анимацией на JavaScript. Например, можно использовать функцию таймера — setTimeout или setInterval — и обновлять стили каждые несколько миллисекунд. Другой подход — создать цикл, который изменяет стили насколько возможно часто в тот период, пока анимация продолжается. Логика обоих подходов такая: дать браузеру большое количество кадров анимации и надеяться на то, что он выдаст плавное движение.

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

В действительности лучше отображать меньшее количество кадров в секунду, но сделать это количество постоянным. Дело в том, что наш глаз воспринимает небольшие отклонения в частоте, и несколько выпавших кадров режут глаз больше, чем более низкое количество кадров в секунду. Вот здесь на помощь приходит встроенный в HTML5 API requestAnimationFrame.

Преимущества requestAnimationFrame

requestAnimationFrame дает браузеру возможность контролировать, сколько кадров он может обработать. Вместо того, чтобы требовать от браузера отображать кадры, которые в итоге выпадут, вы разрешаете ему показывать кадры тогда, когда они обработаны, и с постоянной частотой. Польза от этого двояка:

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

Наконец, если текущий таб браузера перестает быть в фокусе, requestAnimationFrame перестанет выполнять операции по анимации. Это прекрасно влияет на энергосбережение и общую производительность. Боритесь за экологию! Используйте requestAnimationFrame!

Как использовать requestAnimationFrame

Теперь, когда вы понимаете преимущества requestAnimationFrame, я бы хотел погрузиться чуть глубже и показать, как его использовать. Я возьму пример из моей новой книги: «Поднимаем планку в программировании на JavaScript», которую вам стоит почитать, если вы хотите больше узнать о requestAnimationFrame и других продвинутых приемах работы с HTML5.

Для начала напишем разметку:

<div id="my-element">Щелкните, чтобы начать анимацию</div>

Добавим стили:

#my-element {
  position: absolute;
  left: 0;
  width: 200px;
  height: 200px;
  padding: 1em;
  background: tomato;
  color: #FFF;
  font-size: 2em;
  text-align: center;
}

И наконец сам скрипт:

var elem = document.getElementById('my-element'),
    startTime = null,
    endPos = 500, // в пикселях
    duration = 2000; // в миллисекундах

function render(time) {
  if (time === undefined) {
    time = new Date().getTime();
  }
  if (startTime === null) {
    startTime = time;
  }

  elem.style.left = ((time - startTime) / duration * endPos % endPos) + 'px';
}

elem.onclick = function() {
  (function animationLoop(){
    render();
    requestAnimationFrame(animationLoop, elem);
  })();
};

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

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

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

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

Полифилл для обратной совместимости

Полагаю, теперь вы согласитесь, что requestAnimationFrame — прекрасное решение ряда проблем с анимацией. Однако реальность такова, что значительное количество пользователей все еще пользуются браузерами, которые не поддерживают HTML5 и этот API. К счастью, есть полифилл, написанный Эриком Мёллером и Полом Айришем, который поддерживает старые браузеры. Конечно, в старых браузерах вы не увидите преимущество нового API, но по крайней мере в них будет работать анимация — через традиционную функцию таймера.

Для этого просто включите в свой проект следующий код (а прочитать о нем можно здесь):

(function() {
  var lastTime = 0;
  var vendors = ['ms', 'moz', 'webkit', 'o'];
  for(var x = 0; x &lt; vendors.length &amp;&amp; !window.requestAnimationFrame; ++x) {
    window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
    window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
                               || window[vendors[x]+'CancelRequestAnimationFrame'];
  }

  if (!window.requestAnimationFrame)
    window.requestAnimationFrame = function(callback, element) {
      var currTime = new Date().getTime();
      var timeToCall = Math.max(0, 16 - (currTime - lastTime));
      var id = window.setTimeout(function() { callback(currTime + timeToCall); },
        timeToCall);
      lastTime = currTime + timeToCall;
      return id;
    };

  if (!window.cancelAnimationFrame)
    window.cancelAnimationFrame = function(id) {
      clearTimeout(id);
    };
}());

Заключение

Надеюсь, вы были рады узнать о requestAnimationFrame. Это всего лишь одно из многих улучшений внутри HTML5 — и о многих из них можно узнать на HTML5 Hub. Попробуйте этот пример, дайте мне знать, понравилось ли вам — либо в комментариях, либо напишите мне на Twitter: @jonraasch. И обязательно загляните в мою книгу — «Поднимаем планку в программировании на JavaScript», в которой рассказывается о широком спектре профессиональных практик на HTML5 и JavaScript.

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Jon Raasch
Автор:
Jon Raasch
GitHub:
jonraasch
Twitter:
@jonraasch
Сaйт:
http://jonraasch.com/
LinkedIn:
jonraasch
Vlad Andersen

Комментарии (2 комментария, если быть точным)

Автар пользователя
VovanZver

А как организовать выход из цикла? Что бы оставить только один проход?

Автар пользователя
Nedudi

Еще про requestanimationframe можно почитать тут http://html5.by/blog/what-is-requestanimationframe