Чёрная магия трансформов, или об оптимизации анимаций на CSS
Иногда Frontend-разработчики сталкиваются с тем, что для оптимизации производительности нужно написать волшебное свойство translateZ(0) или will-change. Тогда анимации перестанут зависать, ничего не будет лагать и мир станет чуть ярче. ✨
Привет! Меня зовут Даша, я Creative Frontend-разработчик в Red Collar. В статье расскажу, как улучшить производительность страницы путём выноса элементов на композитные слои, какие CSS-свойства для этого использовать и как делать это разумно.
Порядок отрисовки анимации изнутри
Предположим, стоит задача «сделать параллакс». Первая мысль — менять свойство top при скролле. Тогда у нас получается примерно такой код:
const scroll = () => {
percent = window.scrollY / height;
squareArray.forEach((square, i) => {
square.style.top = (i * 20 * percent) + 'px';
})
}
Полный пример смотрите тут.
Выглядит неплохо. Но если элементов будет не 10, а 200, страница начнёт тормозить. Причём, иногда анимация работает как нужно в Chrome, но подтормаживает в Firefox. Почему так происходит?
На каждый кадр анимации браузер проходит следующие шаги:
Пересчитывает размеры и позиции элементов на CPU (пересчёт геометрии или layout).
Заполняет элементы цветами, меняет видимость. В общем, работа с тем, что влияет на визуал (отрисовка или paint).
Размещает элементы относительно друг друга (композиция или composite), выносит все в GPU и показывает на экране.
То есть, анимация хорошо отрабатывает в Chrome, потому что его движок потребляет достаточно памяти, чтобы все быстро пересчитать, в отличии от FireFox.
Оптимальные свойства для отрисовки: transform + will-change
Самый ресурсозатратный шаг — процесс отрисовки (paint). В нашем примере отрисовка запускается на каждом элементе при каждой смене кадра. Не верьте мне, посмотрите сами, включив настройку «paint flashing» в Chrome.
Как включить Paint flashing
Нажимаем на 3 точки (кебаб-меню) в панели разработчика
More tools => Performance monitor
Вкладка Rendering => поставить галочку Paint flashing
Результат:
Получается, когда страница зависает, браузер просто не успевает достаточно быстро пройти шаги 1-3. Если при анимации будем двигать только сам элемент, то браузер пропускает первые 2 шага и сразу перейдет к композиции. Подберем свойства, которые:
никак не влияют на поток документа и не зависят от него;
не требуют отрисовки.
CSS-свойство, подходящее под эти требования, — transform. Оно выделит элемент на отдельный слой и будет затрагивать только композицию.
Переделаем наш код и уберем top:
const scroll = () => {
percent = window.scrollY / height;
squareArray.forEach((square, i) => {
square.style.transform = `translate3d(0, ${i * 20 * percent}px, 0)`
})
}
Также добавим will-change в css, чтобы еще до начала анимации браузер знал, что именно мы будем менять и не производил лишних вычислений:
.elem { will-change: transform; }
Полный пример смотрите тут.
Получаем решение, которое вообще не затрагивает отрисовку — к чему и стремились!
Где лучше не использовать will-change
Итак, мы улучшили производительность страницы путём выноса элементов на композитные слои. Теперь рассмотрим недостатки использования will-change.
Свойство нельзя использовать на всех элемента подряд, обсудим почему. Дело в том, что will-change выносит элемент на отдельный композитный слой, при создании которого выделяется память на GPU (graphics processing unit). Много слоев = большие затраты. Десктоп может и справится, но вот мобильная версия нет: в анимации будет заметно «моргание» или другие артефакты. А может и вообще произойти краш страницы (опаньки!).
Помимо случаев, когда мы вручную выносим что-то на композитный слой, есть и ситуации, в которых браузер делает это сам:
когда имеются дочерние элементы со свойствами transform, opacity (меньше 1), mask, filter, reflection;
когда дочерние элементы скрыты через overflow;
когда композитный слой перекрывается элементом с большим z-index;
при 3D-трансформациях (translate3d, translateZ, perspective и т.д.);
в анимации transform и opacity через Element.animate(), CSS Transitions, Animations;
в элементах <video>, <canvas>, <iframe>, <flash>;
когда имеются свойства will-change, position:fixed, filter, backdrop-filter, backface-visibility: hidden.
Все слои и причину выноса элемента на новый слой можно посмотреть на вкладке Layers.
Как включить Layers:
нажимаем на 3 точки (кебаб-меню) в панели разработчика
More tools => Layers
Выбираем слой, крутим, смотрим как выглядит и на что влияет
Иногда браузер вообще выносит всё на один большой слой. В таком случае при изменении одного элемента приходится пересчитывать и остальные. Чтобы разбить всё на несколько слоев, задавайте уникальные translateZ() для элементов. Например, translateZ(0.001px), translateZ(0.002px).
Только будьте внимательны, в Safari могут быть сюрпризы. Он может игнорировать z-index и путать порядок слоёв.
Вот тут шпаргалка, на которой наглядно можно посмотреть, какие свойства лучше брать. Видно, что свойства width/height или top/left влияют на отрисовку всей страницы (layout) и лучше их не трогать при анимации. А вот opacity и transform влияют только на композицию (composite), анимации на их основе будут самыми быстрыми и плавными.
Случаи, когда JS влияет на отрисовку, разобраны здесь.
Краткие итоги
Мы разобрали, как происходит отрисовка страниц и познали черную магию transform для работы с элементами в композитных слоях. Предложили свойства, которые сделают ваши анимации лучше и спасут от крашей.
Мы рассказали, о том, что нужно знать новичку о рендеринге страниц, если хотите погрузиться в тему глубже, можно начать отсюда: бесплатный курс или в этой статье, или этой статье. Желаем вам успехов и плавных анимаций!