#Запрашивайте кадры анимации для лучшей производительности
Есть несколько известных способов работать с анимацией на 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 < vendors.length && !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.