Изображения и текст принадлежат их авторам.
Анимация элементов в мобильных приложениях — это просто. Правильная анимация тоже может быть простой… если вы последуете представленным в статье советам.
Сегодня кто только не использует CSS 3 анимацию в своих проектах, тем не менее не только лишь все, но мало кто может делать это правильно. Даже описаны так называемые «лучшие практики», но люди продолжают делать всё по-своему. Скорее всего потому, что просто не понимают, почему всё устроено именно так, а не иначе.
Спектр мобильных устройств только и делает, что расширяется, поэтому если вы не оптимизируете свой код с учётом этого, такой подход рано или поздно даст о себе знать в виде тормозов приложения, с которыми будут сталкиваться ваши пользователи.
Запомните: не смотря на то, что в мире есть несколько флагманских девайсов, постоянно толкающих прогресс вперед, люди пользуются своим любимым антиквариатом, с которым им неохота расставаться.
Корректное использование CSS 3 устранит часть проблем, поэтому мы хотим помочь вам в понимании некоторых вещей.
Укрощая время
Что делает браузер в процессе рендеринга и управления всеми этими элементами на странице? Ответ — эта простая временная шкала, названная CRP (Critical Rendering Path).
Чтобы достичь плавности анимаций, нам необходимо сфокусироваться на изменении свойств, которые влияют на шаг Composite.
Стили
Браузер начинает рассчитывать стили, чтобы применить их к элементам.
Каркас
На данном этапе браузер формирует и определяет позицию элементов на странице. Именно в этот момент браузер устанавливает атрибуты страницы, такие как ширина, высота, отступы и другие.
Отрисовка
Браузер формирует элементы как отдельные слои, применяя к ним такие свойства, как box-shadow, border-radius, color, background-color и так далее.
Общая картина
Именно на данном этапе потребуется ваша магия, так как именно сейчас браузер отрисовывает все сформированные слои на экране. Современные браузеры отлично анимируют четыре вида свойств, оперируя трансформацией и полупрозрачностью.
- Позиция. transform: translateX(n) translateY(n) translateZ(n);
- Масштабирование. transform: scale(n);
- Поворот. transform: rotate(ndeg);
- Полупрозрачность. opacity: n;
Как достичь отметки в 60 FPS
Давайте начнем с HTML и создадим простую структуру для меню приложения внутри контейнера .layout
.
<div class="layout">
<div class=”app-menu”></div>
<div class=”header”></div>
</div>
Неправильный путь
.app-menu {
left: -300px;
transition: left 300ms linear;
}
.app-menu-open .app-menu {
left: 0px;
transition: left 300ms linear;
}
Заметили, какие свойства мы меняем? Необходимо избегать использования трансформаций со свойствами left/top/right/bottom. Они не позволяют создавать плавную анимацию, потому что заставляют браузер пересобирать слои каждый раз, а это подействует на все дочерние элементы.
Результат примерно такой:
Анимация тормозит. Мы проверили временную шкалу DevTools, чтобы посмотреть, что происходит на самом деле, и вот результат:
Картинка ясно показывает нестабильность FPS и, как следствие, низкую производительность.
Использование трансформаций
app-menu {
-webkit-transform: translateX(-100%);
transform: translateX(-100%);
transition: transform 300ms linear;
}
.app-menu-open .app-menu {
-webkit-transform: none;
transform: none;
transition: transform 300ms linear;
}
В отличие от вышеуказанных свойств, трансформации применяются к уже отрисованным блокам, то есть на стадии Composite. В примере выше мы как бы говорим браузеру, что перед началом анимации все слои уже будут отрисованы и готовы к манипуляциям.
Временная шкала показывает, что FPS стал более ровным, поэтому и анимация будет выглядеть несколько плавнее.
Анимация с использованием GPU
Всё может быть ещё лучше. Для этого мы будем использовать графический ускоритель.
.app-menu {
-webkit-transform: translateX(-100%);
transform: translateX(-100%);
transition: transform 300ms linear;
will-change: transform;
}
В то время как translateZ() или translate3d() всё еще требуются некоторыми браузерами, как некий хак, будущее за свойством will-change. Оно указывает браузеру переместить элементы в отдельный слой так, чтобы он затем не проверял весь каркас на предмет сборки или отрисовки.
Видите, насколько плавной стала анимация? Таймлайн это подтверждает:
FPS стал ещё более стабильным, но всё же еще остается один медленный отрезок анимации в начале. Исходя из структуры меню, в JS обычно пишут примерно такую обработку:
function toggleClassMenu() {
var layout = document.querySelector(".layout");
if(!layout.classList.contains("app-menu-open")) {
layout.classList.add("app-menu-open");
} else {
layout.classList.remove("app-menu-open");
}
}
var menu = document.querySelector(".menu-icon");
menu.addEventListener("click", toggleClassMenu, false);
Проблема в том, что добавляя класс к контейнеру .layout
, мы заставляем браузер пересчитывать стили еще раз, а это сказывается на скорости компоновки и отрисовки.
Как по маслу
Но что если бы меню было расположено за областью видимости? Сделав это, мы задействовали бы только тот элемент, который действительно необходимо анимировать, то есть наше меню. Для ясности — структура HTML:
<div class="menu">
<div class="app-menu"></div>
</div>
<div class="layout">
<div class="header"></div>
</div>
Теперь мы можем контролировать состояние меню несколько по-другому. Мы будем управлять анимацией через класс, который удаляется после того, как анимация заканчивается, используя событие transitionend.
function toggleClassMenu() {
myMenu.classList.add("menu--animatable");
myMenu.classList.add("menu--visible");
}
function onTransitionEnd() {
myMenu.classList.remove("menu--animatable");
}
var myMenu = document.querySelector(".menu"),
menu = document.querySelector(".menu-icon");
myMenu.addEventListener("transitionend", onTransitionEnd, false);
menu.addEventListener("click", toggleClassMenu, false);
Ну, а теперь всё вместе. Вашему вниманию полный пример CSS 3, где всё на своих местах:
.menu {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 150;
}
.menu—visible {
pointer-events: auto;
}
.app-menu {
background-color: #fff;
color: #fff;
position: relative;
max-width: 400px;
width: 90%;
height: 100%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
-webkit-transform: translateX(-103%);
transform: translateX(-103%);
display: flex;
flex-direction: column;
will-change: transform;
z-index: 160;
pointer-events: auto;
}
.menu—-visible.app-menu {
-webkit-transform: none;
transform: none;
}
.menu-—animatable.app-menu {
transition: all 130ms ease-in;
}
.menu--visible.menu—-animatable.app-menu {
transition: all 330ms ease-out;
}
.menu:after {
content: ‘’;
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.4);
opacity: 0;
will-change: opacity;
pointer-events: none;
transition: opacity 0.3s cubic-bezier(0,0,0.3,1);
}
.menu.menu--visible:after{
opacity: 1;
pointer-events: auto;
}
А что показывает временная шкала?
Как-то так.
Комментарии (27)
Ronnie_Gardocki
18.08.2016 09:26-3Ухты, webperf 101 в 2016 году на хабре, вот это да!
И еще забавно читать про «будущее за will-change». Это казалось будущим 2 года назад, но по итогам от этого свойства было скорее больше проблем, чем пользы, и оно все еще не поддерживается в IE/Edge, так что лучше по-стариночке юзать translate3d() там, где это необходимо.
GreatRash
18.08.2016 09:53Так и знал, что будет использовано какое-то магическое свойство, которое в половине браузеров не работает. Ну и за одно выдержки с MDN:
— Не применяйте will-change к большому числу элементов.
— Используйте умеренно.
— Не применяйте will-change к элементам для выполнения преждевременной оптимизации.
— Дайте ему достаточно времени, чтобы работать.
uralmas
18.08.2016 09:59+2А насколько такой вариант кроссбраузный? Просто вариант с изменением ширины поддерживают все браузеры. Интересует практическое применение на мобильных устройствах
slonopotamus
18.08.2016 10:39Вам не кажется, что что-то не так с технологией где нужны особые специальные ухищрения чтобы плавно подвинуть *один прямоугольник*?
bjornd
18.08.2016 11:33На самом деле все что нужно сделать чтобы плавно подвинуть *один прямоугольник*:
.app-menu { transform: translateX(-100%); transition: transform 300ms linear; }
demimurych
18.08.2016 13:41На самом деле это далеко не так.
Начиная от того что в Вашем примере не используются 3d трансформации, и заканчивая тем что Вы используете проценты.
Создайте страницу с большим количеством тегов и посмотрите на Ваш пример в дев тулс.
После этого примените 3d трансформацию но оставьте проценты
И после этого замените проценты на пиксели.Odrin
18.08.2016 14:04+1Использование процентов может быть необходимостью в некоторых условиях, но в любом случае:
.app-menu { transform: translate3d(-100px, 0, 0); transition: transform 300ms linear; width: 100px; }
Этого уже достаточно и с производительностью все ok. А дичь, которую предлагают в конце статьи — это какие-то извращения с костылями. И не факт, что через n месяцев оно по прежнему будет выигрывать в производительности, потому что оптимизировать будут именно приведенный выше код.
kahi4
18.08.2016 12:13+6will-change. Оно указывает браузеру переместить элементы в отдельный слой так, чтобы он затем не проверял весь каркас на предмет сборки или отрисовки.
Нет, оно работает несколько иначе. Дело в том, что когда применяется анимация с transform, браузер копирует отрисованный элемент в виде текстуры и двигает уже непосредственно её. Однако, покуда он не знает, когда будут проводиться эти самые анимации, а держать и перерисовывать в памяти кучу подобных текстурок накладно, он это по-умолчанию делает в момент когда анимация началась, именно по этому он может (и обычно делает это) подвиснуть вначале анимации и сделать её не плавной. will-change говорит ему о том, что этот объект будет изменяться через animation и эту процедуру рендера в текстуру необходимо сделать заранее. Вот только после этого ему постоянно нужно проделывать достаточно много лишней работы (даже если вы просто будете водить курсором по пунктам меню), а так же будет жрать лишнюю память. Как следствие — это свойство лучше всего использовать ровно перед тем, как сама анимация будет совершена, например, навешивать это свойство при ховере, иначе пользователи будут вам благодарны, что у них браузер анимации воспроизводит плавно, но памяти стал есть в два раза больше.
P.S. На сколько я знаю, справедливо для webkit, gecko и как там у Edge движок называется, вероятно, весь процесс реализован иначе.tenbits
18.08.2016 12:57это свойство лучше всего использовать ровно перед тем, как сама анимация будет совершена, например, навешивать это свойство при ховере
А как поступать, если мы открываем меню по нажатию горячих клавиш, на пример "m"? Добавлять свойство на keydown, а на keyup уже показывать? Но что если нужно показать непосредственно сразу при нажатии?
kahi4
18.08.2016 13:22Спешу себя поправить по поводу «рендера в текстурку». Тут, возможно, я несколько погорячился, рендер в текстуру происходит не всегда, суть действительно в работе со слоями, но я боюсь даже представить насколько хитро работает внутри оптимизации. В текстуру он рендерит именно при афинных преобразованиях и прочих, производимых на видеокарте, не затрагивающих DOM (не приводящую к его пересчету). Это при медленных анимациях очень хорошо видно, покуда он применяет обычное сглаживание, характерное для текстур, а не pixel-perfect (т.е. черная линия в 1px будет выглядеть как серая в 2px).
Статья о том, как правильно пользоваться свойством will-change была как-то тут же на хабре, но, походу, канула в небытие, так что нашел похожую на dev.opera. Что касается горячих клавиш и, например, мобильных устройств — во втором случае все равно есть задержка в 40+ мс между событиями onTouch и onClick (не помню точно набор событий, но не суть), можно вешать на onTouch, а если пользователь не кликнул, а решил скроллить — отвешивать обратно. В случае горячих клавиш — вообще не использовать, оставлять это свойство только на тех элементах, которые с большей частью вероятности будут анимированы.Zavtramen
18.08.2016 17:55Для случая горячих клавиш можно просто сделать небольшой таймаут, скажем в 40мс. Глаз этого не заметит, а will-change успеет отработать.
Serator
20.08.2016 01:42А зачем тогда, собственно,
will-change
, если браузер все равно проведет подготовительные работы во время трансформации? Вся проблемаwill-change
в том, что его сложно использовать. Мы все привыкли ко всяким там "непонятным" эвристикам и т.п., что браузер все за нас делает и т.п., и т.д. А тут появилсяwill-change
(иcontains
;)) и разработчики браузеров предложили нам все самим делать в помощь браузеру (увы, не во всех сценариях браузер может все верно оптимизировать и оптимизировать ли?..). kahi4, в целом, неплохо все объяснил. Дополню, чтоwill-change
нужен не только во время анимации, но и при любом ином изменении, а не только CSStransform
. То, что делает это свойство, будет и без него проделано, но на эти подготовительные работы нужны ресурсы (время и мощности железа). При открытии меню, к примеру, мы хотим увидеть начало анимации сразу же после клика на кнопку и эти подготовительные работы отсрочат начало анимации на n миллисекунд, что будет замечено пользователем и испортит пользовательский опыт. Вот чтобы этого не происходило, посредствомwill-change
можно подготовить элемент заранее. :)
Finesse
18.08.2016 14:04Понятно, почему вынос меню вне .layout и применение transform увеличивают плавность, но совсем не понятно, почему добавление класса на время анимации делает это. Почему transition нет в .app-menu постоянно?
P.S. В коде в статье много семантических ошибок, советую смотреть код в оригинальной статье.
noodles
19.08.2016 00:52у меня в шпаргалке по оптимизации анимаций ещё такие штуки записаны, может кто захочет использовать:
backface-visibility: hidden; (to the animation's parent element. The browser will think you're going to do some 3D transforms and takes measures to help keep things at a silky smooth 60fps.)
а это конкретно для @keyframes — animation-play-state: paused; (Set the animation's parent element's animation-play-state to paused and all its children to inherit. и когда нужно убираем паузу с помощью js, так типа анимация уже запущена просто на паузе)
Заголовок спойлеразачем if-ом проверять наличие класса? classList.toggle(«класс») не… не слышал?..))Ronnie_Gardocki
19.08.2016 18:32backface-visiblity: hidden уже давно ничего не триггерит в хроме (и скорее всегов фф). Год+ назад еще может работало, сейчас оно делает лишь то что должно, без 3д-акселерации.
ixside
Можете сделать ссылочку на jsfiddle? Скажу спасибо
franzose
А есть ли возможность в саму статью вставить блок CodePen? Я не знаю, как это делается, а в исходной статье он есть.
phantomd
Пример. Это из оригинала статьи