Pull to refresh

Как по маслу, или анимируем со скоростью 60 FPS на CSS 3

Reading time 5 min
Views 61K
Original author: Хосе Розарио

Изображения и текст принадлежат их авторам.


Анимация элементов в мобильных приложениях — это просто. Правильная анимация тоже может быть простой… если вы последуете представленным в статье советам.


Сегодня кто только не использует CSS 3 анимацию в своих проектах, тем не менее не только лишь все, но мало кто может делать это правильно. Даже описаны так называемые «лучшие практики», но люди продолжают делать всё по-своему. Скорее всего потому, что просто не понимают, почему всё устроено именно так, а не иначе.



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


Запомните: не смотря на то, что в мире есть несколько флагманских девайсов, постоянно толкающих прогресс вперед, люди пользуются своим любимым антиквариатом, с которым им неохота расставаться.


Корректное использование CSS 3 устранит часть проблем, поэтому мы хотим помочь вам в понимании некоторых вещей.


Укрощая время


Что делает браузер в процессе рендеринга и управления всеми этими элементами на странице? Ответ — эта простая временная шкала, названная CRP (Critical Rendering Path).


image

Чтобы достичь плавности анимаций, нам необходимо сфокусироваться на изменении свойств, которые влияют на шаг Composite.


Стили


image

Браузер начинает рассчитывать стили, чтобы применить их к элементам.


Каркас


image

На данном этапе браузер формирует и определяет позицию элементов на странице. Именно в этот момент браузер устанавливает атрибуты страницы, такие как ширина, высота, отступы и другие.


Отрисовка


image

Браузер формирует элементы как отдельные слои, применяя к ним такие свойства, как box-shadow, border-radius, color, background-color и так далее.


Общая картина


image

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


  1. Позиция. transform: translateX(n) translateY(n) translateZ(n);
  2. Масштабирование. transform: scale(n);
  3. Поворот. transform: rotate(ndeg);
  4. Полупрозрачность. opacity: n;

Как достичь отметки в 60 FPS


Давайте начнем с HTML и создадим простую структуру для меню приложения внутри контейнера .layout.


<div class="layout">
    <div class=”app-menu”></div>
    <div class=”header”></div>
</div>

image

Неправильный путь


.app-menu {
  left: -300px;
  transition: left 300ms linear;
}

.app-menu-open .app-menu {
  left: 0px;
  transition: left 300ms linear;
}

Заметили, какие свойства мы меняем? Необходимо избегать использования трансформаций со свойствами left/top/right/bottom. Они не позволяют создавать плавную анимацию, потому что заставляют браузер пересобирать слои каждый раз, а это подействует на все дочерние элементы.


Результат примерно такой:


image

Анимация тормозит. Мы проверили временную шкалу DevTools, чтобы посмотреть, что происходит на самом деле, и вот результат:


image

Картинка ясно показывает нестабильность 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. В примере выше мы как бы говорим браузеру, что перед началом анимации все слои уже будут отрисованы и готовы к манипуляциям.


image

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


image

Анимация с использованием GPU


Всё может быть ещё лучше. Для этого мы будем использовать графический ускоритель.


.app-menu {
  -webkit-transform: translateX(-100%);
          transform: translateX(-100%);
  transition: transform 300ms linear;
  will-change: transform;
}

В то время как translateZ() или translate3d() всё еще требуются некоторыми браузерами, как некий хак, будущее за свойством will-change. Оно указывает браузеру переместить элементы в отдельный слой так, чтобы он затем не проверял весь каркас на предмет сборки или отрисовки.


image

Видите, насколько плавной стала анимация? Таймлайн это подтверждает:


image

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;
}

image

А что показывает временная шкала?


image

Как-то так.

Tags:
Hubs:
+47
Comments 27
Comments Comments 27

Articles