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

https://medium.com/outsystems-experts/how-to-achieve-60-fps-animations-with-css3-db7b98610108
  • Перевод
  • Tutorial

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


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


Сегодня кто только не использует 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

Как-то так.

Поделиться публикацией
Комментарии 27
    +3
    Можете сделать ссылочку на jsfiddle? Скажу спасибо
      0
      А есть ли возможность в саму статью вставить блок CodePen? Я не знаю, как это делается, а в исходной статье он есть.
        +1
        Пример. Это из оригинала статьи
        –3
        Ухты, webperf 101 в 2016 году на хабре, вот это да!
        И еще забавно читать про «будущее за will-change». Это казалось будущим 2 года назад, но по итогам от этого свойства было скорее больше проблем, чем пользы, и оно все еще не поддерживается в IE/Edge, так что лучше по-стариночке юзать translate3d() там, где это необходимо.
          +3
          Повторение — мать учения. И новичкам будет полезно)
            +1
            del
            0
            Так и знал, что будет использовано какое-то магическое свойство, которое в половине браузеров не работает. Ну и за одно выдержки с MDN:

            — Не применяйте will-change к большому числу элементов.
            — Используйте умеренно.
            — Не применяйте will-change к элементам для выполнения преждевременной оптимизации.
            — Дайте ему достаточно времени, чтобы работать.
              +2
              А насколько такой вариант кроссбраузный? Просто вариант с изменением ширины поддерживают все браузеры. Интересует практическое применение на мобильных устройствах
                0
                Вам не кажется, что что-то не так с технологией где нужны особые специальные ухищрения чтобы плавно подвинуть *один прямоугольник*?
                  0
                  Я лично считаю, что Москва не сразу строилась. И на то есть вполне объективные причины.
                    0
                    Причина одна. Отсутствие обратной совместимости. Но меня интересует другое. Центрирование дочерних дивов тоже так можно делать? А то с JS кошмар. Аааа-а!
                    0
                    На самом деле все что нужно сделать чтобы плавно подвинуть *один прямоугольник*:
                    .app-menu {
                      transform: translateX(-100%);
                      transition: transform 300ms linear;
                    }
                    
                      0
                      На самом деле это далеко не так.
                      Начиная от того что в Вашем примере не используются 3d трансформации, и заканчивая тем что Вы используете проценты.
                      Создайте страницу с большим количеством тегов и посмотрите на Ваш пример в дев тулс.
                      После этого примените 3d трансформацию но оставьте проценты
                      И после этого замените проценты на пиксели.
                        +1
                        Использование процентов может быть необходимостью в некоторых условиях, но в любом случае:
                        .app-menu {
                          transform: translate3d(-100px, 0, 0);
                          transition: transform 300ms linear;
                          width: 100px;
                        }
                        

                        Этого уже достаточно и с производительностью все ok. А дичь, которую предлагают в конце статьи — это какие-то извращения с костылями. И не факт, что через n месяцев оно по прежнему будет выигрывать в производительности, потому что оптимизировать будут именно приведенный выше код.
                      +2
                      Ок, давайте запретим CSS. Я создам петицию.
                      +6
                      will-change. Оно указывает браузеру переместить элементы в отдельный слой так, чтобы он затем не проверял весь каркас на предмет сборки или отрисовки.


                      Нет, оно работает несколько иначе. Дело в том, что когда применяется анимация с transform, браузер копирует отрисованный элемент в виде текстуры и двигает уже непосредственно её. Однако, покуда он не знает, когда будут проводиться эти самые анимации, а держать и перерисовывать в памяти кучу подобных текстурок накладно, он это по-умолчанию делает в момент когда анимация началась, именно по этому он может (и обычно делает это) подвиснуть вначале анимации и сделать её не плавной. will-change говорит ему о том, что этот объект будет изменяться через animation и эту процедуру рендера в текстуру необходимо сделать заранее. Вот только после этого ему постоянно нужно проделывать достаточно много лишней работы (даже если вы просто будете водить курсором по пунктам меню), а так же будет жрать лишнюю память. Как следствие — это свойство лучше всего использовать ровно перед тем, как сама анимация будет совершена, например, навешивать это свойство при ховере, иначе пользователи будут вам благодарны, что у них браузер анимации воспроизводит плавно, но памяти стал есть в два раза больше.

                      P.S. На сколько я знаю, справедливо для webkit, gecko и как там у Edge движок называется, вероятно, весь процесс реализован иначе.
                        0
                        Ценный комментарий, спасибо!
                          0
                          это свойство лучше всего использовать ровно перед тем, как сама анимация будет совершена, например, навешивать это свойство при ховере

                          А как поступать, если мы открываем меню по нажатию горячих клавиш, на пример "m"? Добавлять свойство на keydown, а на keyup уже показывать? Но что если нужно показать непосредственно сразу при нажатии?

                            0
                            Спешу себя поправить по поводу «рендера в текстурку». Тут, возможно, я несколько погорячился, рендер в текстуру происходит не всегда, суть действительно в работе со слоями, но я боюсь даже представить насколько хитро работает внутри оптимизации. В текстуру он рендерит именно при афинных преобразованиях и прочих, производимых на видеокарте, не затрагивающих DOM (не приводящую к его пересчету). Это при медленных анимациях очень хорошо видно, покуда он применяет обычное сглаживание, характерное для текстур, а не pixel-perfect (т.е. черная линия в 1px будет выглядеть как серая в 2px).

                            Статья о том, как правильно пользоваться свойством will-change была как-то тут же на хабре, но, походу, канула в небытие, так что нашел похожую на dev.opera. Что касается горячих клавиш и, например, мобильных устройств — во втором случае все равно есть задержка в 40+ мс между событиями onTouch и onClick (не помню точно набор событий, но не суть), можно вешать на onTouch, а если пользователь не кликнул, а решил скроллить — отвешивать обратно. В случае горячих клавиш — вообще не использовать, оставлять это свойство только на тех элементах, которые с большей частью вероятности будут анимированы.
                              0
                              Для случая горячих клавиш можно просто сделать небольшой таймаут, скажем в 40мс. Глаз этого не заметит, а will-change успеет отработать.
                                0

                                А зачем тогда, собственно, will-change, если браузер все равно проведет подготовительные работы во время трансформации? Вся проблема will-change в том, что его сложно использовать. Мы все привыкли ко всяким там "непонятным" эвристикам и т.п., что браузер все за нас делает и т.п., и т.д. А тут появился will-changecontains ;)) и разработчики браузеров предложили нам все самим делать в помощь браузеру (увы, не во всех сценариях браузер может все верно оптимизировать и оптимизировать ли?..). kahi4, в целом, неплохо все объяснил. Дополню, что will-change нужен не только во время анимации, но и при любом ином изменении, а не только CSS transform. То, что делает это свойство, будет и без него проделано, но на эти подготовительные работы нужны ресурсы (время и мощности железа). При открытии меню, к примеру, мы хотим увидеть начало анимации сразу же после клика на кнопку и эти подготовительные работы отсрочат начало анимации на n миллисекунд, что будет замечено пользователем и испортит пользовательский опыт. Вот чтобы этого не происходило, посредством will-change можно подготовить элемент заранее. :)

                                  0
                                  Задержка до 100мс пользователями не воспринимается как задержка.
                                    0

                                    На подготовку слоев может уйти и больше. ;) Впрочем, я сего и не отрицал, хоть и не являюсь приверженцем данной теории...

                          0
                          Понятно, почему вынос меню вне .layout и применение transform увеличивают плавность, но совсем не понятно, почему добавление класса на время анимации делает это. Почему transition нет в .app-menu постоянно?

                          P.S. В коде в статье много семантических ошибок, советую смотреть код в оригинальной статье.
                            0
                            у меня в шпаргалке по оптимизации анимаций ещё такие штуки записаны, может кто захочет использовать:

                            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(«класс») не… не слышал?..))
                              0
                              backface-visiblity: hidden уже давно ничего не триггерит в хроме (и скорее всегов фф). Год+ назад еще может работало, сейчас оно делает лишь то что должно, без 3д-акселерации.
                              –5
                              Я один прочитал «анимируем» в заголовке как «онанируем»? :)

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое