Введение
Вы хотите, чтобы ваше веб-приложение было отзывчивым и плавным при выполнении анимации, переходов и других небольших эффектов пользовательского интерфейса. Обеспечение отсутствия рывков в этих эффектах может означать разницу между «родным» ощущением или неуклюжим, неотшлифованным.
Это первая из серии статей, посвященных оптимизации производительности рендеринга в браузере. Для начала мы расскажем, почему плавная анимация сложна и что нужно для этого сделать, а также рассмотрим несколько простых рекомендаций. Многие из этих идей были первоначально представлены в «Jank Busters», докладе, который мы с Натом Дукой провели на конференции Google I/O ( видео ) в этом году.
Представляем вертикальную синхронизацию
Компьютерные геймеры, возможно, знакомы с этим термином, но в Интернете он встречается редко: что такое вертикальная синхронизация ?
Рассмотрим дисплей вашего телефона: он обновляется через регулярные промежутки времени, обычно (но не всегда!) примерно 60 раз в секунду. Вертикальная синхронизация (или вертикальная синхронизация) относится к практике создания новых кадров только между обновлениями экрана. Вы можете думать об этом как о состоянии гонки между процессом, который записывает данные в экранный буфер, и операционной системой, читающей эти данные, чтобы вывести их на дисплей. Мы хотим, чтобы содержимое буферизованного кадра менялось между этими обновлениями, а не во время них; в противном случае монитор будет отображать половину одного кадра и половину другого, что приведет к « разрывам ».
Чтобы получить плавную анимацию, вам нужен новый кадр, который будет готов каждый раз при обновлении экрана. Это имеет два важных значения: время формирования кадра (то есть, когда кадр должен быть готов) и бюджет кадра (то есть, как долго браузер должен создавать кадр). У вас есть время между обновлениями экрана только для завершения кадра (~ 16 мс на экране с частотой 60 Гц), и вы хотите начать создание следующего кадра, как только последний был показан на экране.
Время решает все: requestAnimationFrame
Многие веб-разработчики используют setInterval
или setTimeout
каждые 16 миллисекунд для создания анимации. Это проблема по ряду причин (подробнее мы обсудим через минуту), но особую озабоченность вызывают следующие:
- Разрешение таймера в JavaScript составляет всего лишь порядка нескольких миллисекунд.
- Разные устройства имеют разную частоту обновления
Вспомните упомянутую выше проблему синхронизации кадров: вам нужен законченный кадр анимации, в котором есть все JavaScript, манипуляции с DOM, макет, рисование и т. д., чтобы он был готов до того, как произойдет следующее обновление экрана. Низкое разрешение таймера может затруднить завершение кадров анимации до следующего обновления экрана, но изменение частоты обновления экрана делает это невозможным при использовании фиксированного таймера. Независимо от интервала таймера, вы будете медленно выходить из временного окна для кадра и в конечном итоге потеряете один. Это произойдет, даже если таймер сработает с точностью до миллисекунды, чего не произойдет (как обнаружили разработчики) — разрешение таймера варьируется в зависимости от того, работает ли машина от батареи или от сети, на нее могут влиять фоновые вкладки, захватывающие ресурсы, и т. д. Даже если это случается редко (скажем, каждые 16 кадров из-за отклонения на миллисекунду), вы заметите: вы будете терять несколько кадров в секунду. Вы также будете выполнять работу по созданию кадров, которые никогда не отображаются, что приводит к потере мощности и времени процессора, которые вы могли бы потратить на другие действия в своем приложении.
Разные дисплеи имеют разную частоту обновления: обычно 60 Гц, но в некоторых телефонах частота обновления составляет 59 Гц, в некоторых ноутбуках частота обновления снижается до 50 Гц, в некоторых настольных мониторах — 70 Гц.
Мы склонны сосредотачиваться на количестве кадров в секунду (FPS), когда обсуждаем производительность рендеринга, но дисперсия может стать еще большей проблемой. Наши глаза замечают крошечные, нерегулярные заминки в анимации, которые могут возникнуть из-за плохо рассчитанной по времени анимации.
Чтобы получить правильно синхронизированные кадры анимации, используйте requestAnimationFrame
. Когда вы используете этот API, вы запрашиваете у браузера кадр анимации. Ваш обратный вызов вызывается, когда браузер скоро создаст новый кадр. Это происходит независимо от частоты обновления.
requestAnimationFrame
есть и другие приятные свойства:
- Анимация на фоновых вкладках приостанавливается, что экономит системные ресурсы и время автономной работы.
- Если система не может обрабатывать рендеринг с частотой обновления экрана, она может регулировать анимацию и выполнять обратный вызов реже (скажем, 30 раз в секунду на экране с частотой 60 Гц). Хотя это снижает частоту кадров вдвое, анимация остается неизменной — и, как говорилось выше, наши глаза гораздо больше настроены на дисперсию, чем на частоту кадров. Стабильная частота 30 Гц выглядит лучше, чем частота 60 Гц, при которой пропускается несколько кадров в секунду.
requestAnimationFrame
уже обсуждался повсюду, поэтому обратитесь к статьям Creative JS, подобным этой, для получения дополнительной информации о нем, но это важный первый шаг к сглаживанию анимации.
Рамочный бюджет
Поскольку мы хотим, чтобы новый кадр был готов при каждом обновлении экрана, между обновлениями есть только время, чтобы выполнить всю работу по созданию нового кадра. На дисплее с частотой 60 Гц это означает, что у нас есть около 16 мс для запуска всего JavaScript, макетирования, рисования и всего остального, что браузер должен сделать, чтобы вывести кадр. Это означает, что если выполнение JavaScript внутри вашего обратного вызова requestAnimationFrame
занимает больше 16 мс, у вас нет никакой надежды создать кадр вовремя для v-sync!
16 мс — это не так уж и много. К счастью, инструменты разработчика Chrome могут помочь вам отследить, не расходуете ли вы бюджет кадров во время обратного вызова requestAnimationFrame.
Если открыть временную шкалу Dev Tools и записать эту анимацию в действии, то сразу видно, что при анимации мы значительно превышаем бюджет. На временной шкале переключитесь на «Кадры» и посмотрите:
![Демо со слишком большим макетом](https://web.dev/static/articles/speed-rendering/image/a-demo-far-too-much-layo-d53c622ced846.png?authuser=5&hl=ru)
Эти обратные вызовы requestAnimationFrame (rAF) занимают> 200 мс. Это на порядок слишком долго, чтобы отмечать кадр каждые 16 мс! Открытие одного из этих длинных обратных вызовов rAF показывает, что происходит внутри: в данном случае много макета.
В видео Пола более подробно рассказывается о конкретной причине ретрансляции (читается scrollTop
») и о том, как ее избежать. Но суть здесь в том, что вы можете погрузиться в обратный вызов и выяснить, что занимает так много времени.
![Обновленная демоверсия со значительно уменьшенным макетом.](https://web.dev/static/articles/speed-rendering/image/an-updated-demo-much-red-4918377a3889.png?authuser=5&hl=ru)
Обратите внимание на время кадра 16 мс. Это пустое пространство в кадрах — это запас, который вам нужно проделать больше (или позволить браузеру выполнять необходимую работу в фоновом режиме). Это пустое пространство – это хорошо.
Другой источник Джанка
Самая большая причина проблем при попытке запуска анимации на основе JavaScript заключается в том, что другие вещи могут помешать вашему обратному вызову rAF или даже вообще помешать его запуску. Даже если ваш обратный вызов rAF невелик и выполняется всего за несколько миллисекунд, другие действия (например, обработка только что поступившего XHR, запуск обработчиков входных событий или запуск запланированных обновлений по таймеру) могут внезапно возникнуть и выполняться в течение любого периода времени. время, не сдаваясь. На мобильных устройствах обработка этих событий иногда может занимать сотни миллисекунд, в течение которых ваша анимация будет полностью остановлена. Мы называем эти заминки в анимации джанк .
Не существует волшебного средства, позволяющего избежать таких ситуаций, но есть несколько лучших архитектурных практик, которые помогут вам добиться успеха:
- Не выполняйте большую обработку в обработчиках ввода! Выполнение большого количества кода JS или попытка переупорядочить всю страницу во время, например, обработчика прокрутки, является очень распространенной причиной ужасных зависаний.
- Поместите как можно больше обработки (читай: все, что займет много времени) в обратный вызов rAF или веб-воркеры .
- Если вы помещаете работу в обратный вызов rAF, попробуйте разбить ее на части, чтобы обрабатывать каждый кадр только понемногу, или отложите ее до тех пор, пока не закончится важная анимация - таким образом вы можете продолжать выполнять короткие обратные вызовы rAF и плавно анимировать. .
Отличное руководство о том, как перенести обработку в обратные вызовы requestAnimationFrame, а не в обработчики ввода, см. в статье Пола Льюиса Leaner, Meaner, Faster Animations with requestAnimationFrame .
CSS-анимация
Что может быть лучше, чем легкий JS для ваших событий и обратные вызовы RAF? Нет ДжС.
Ранее мы говорили, что не существует серебряной пули, позволяющей избежать прерывания обратных вызовов rAF, но вы можете использовать анимацию CSS, чтобы полностью избежать необходимости в них. В частности, в Chrome для Android (и другие браузеры работают над аналогичными функциями) CSS-анимация обладает тем очень желательным свойством, что браузер часто может запускать ее, даже если запущен JavaScript.
В приведенном выше разделе о спаме есть неявное утверждение: браузеры могут делать только одно действие одновременно. Это не совсем так, но это хорошее рабочее предположение: в любой момент времени браузер может запускать JS, выполнять верстку или рисование, но только по одному. Это можно проверить в представлении временной шкалы Dev Tools. Одним из исключений из этого правила является CSS-анимация в Chrome для Android (а вскоре и в настольном Chrome, хотя пока еще).
Если это возможно, использование CSS-анимации упрощает ваше приложение и позволяет анимации работать плавно, даже во время работы JavaScript.
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
Если вы нажмете кнопку, JavaScript будет работать в течение 180 мс, что приведет к зависанию. Но если вместо этого мы управляем этой анимацией с помощью CSS-анимации, зависаний больше не происходит.
(Помните, что на момент написания этой статьи анимация CSS не прерывалась только в Chrome для Android, но не в Chrome для настольных компьютеров.)
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
Дополнительную информацию об использовании CSS-анимации см. в подобных статьях на MDN .
Заворачивать
Вкратце это:
- При анимации важно создавать кадры для каждого обновления экрана. Анимация Vsync оказывает огромное положительное влияние на восприятие приложения.
- Лучший способ получить анимацию с вертикальной синхронизацией в Chrome и других современных браузерах — использовать анимацию CSS. Если вам нужна большая гибкость, чем обеспечивает анимация CSS, лучшим методом является анимация на основе requestAnimationFrame.
- Чтобы анимация rAF оставалась здоровой и приятной, убедитесь, что другие обработчики событий не мешают выполнению обратного вызова rAF, и делайте обратные вызовы rAF короткими (<15 мс).
Наконец, анимация с использованием vsync применима не только к простой анимации пользовательского интерфейса — она применима к анимации Canvas2D, анимации WebGL и даже к прокрутке статических страниц. В следующей статье этой серии мы углубимся в производительность прокрутки с учетом этих концепций.
Приятной анимации!