Иногда 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. Почему так происходит?

На каждый кадр анимации браузер проходит следующие шаги:

  1. Пересчитывает размеры и позиции элементов на CPU (пересчёт геометрии или layout).

  2. Заполняет элементы цветами, меняет видимость. В общем, работа с тем, что влияет на визуал (отрисовка или paint).

  3. Размещает элементы относительно друг друга (композиция или 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 для работы с элементами в композитных слоях. Предложили свойства, которые сделают ваши анимации лучше и спасут от крашей.

Мы рассказали, о том, что нужно знать новичку о рендеринге страниц, если хотите погрузиться в тему глубже, можно начать отсюда: бесплатный курс или в этой статье, или этой статье. Желаем вам успехов и плавных анимаций!