Производительный параллакс
13.04.2017

Производительный параллакс

Оригинал: Performant Parallaxing, Paul Lewis

Любите вы параллакс или ненавидите его, но он никуда не денется. При разумном использовании он может добавить веб-приложению глубину и утонченность. Проблема заключается в том, что реализовать параллакс производительно — это непросто. В этой статье мы обсудим решение, которое одновременно и производительное, и, что важно, кросс-браузерное.

TL;DR

  • Не используйте события прокрутки или background-position для реализации параллакса.
  • Для более правильного эффекта параллакса используйте CSS 3D трансформации.
  • Для Mobile Safari используйте position: sticky для обеспечения распространения эффекта параллакса.

Если вам нужно готовое решение, возьмите Parallax helper JS из репозитория UI Element Samples! Вы можете посмотреть живое демо.

Проблемы параллакса

Для начала рассмотрим два общих способа реализации эффекта параллакса, в частности, почему они нам не подходят.

Плохо: использование событий прокрутки

Ключевое требование параллакса — он должен быть связан с прокруткой страницы; каждому изменению позиции скролла должно соответствовать изменение позиции элементов, к которым применяется параллакс. Это звучит просто, но важный механизм современных браузеров — это возможность работать асинхронно. Это применимо, в нашем случае, к событию scroll. В большинстве браузеров обработчики события scroll выполняются по принципу «лучшее из возможного» (в оригинале — «best-effort», прим. переводчика) и не гарантируют анимацию при каждом изменении положения скролла!

Эта важная информация говорит нам о том, почему мы должны избегать основанного на JavaScript решения, которое перемещает элементы на основе событий прокрутки: JavaScript не гарантирует, что параллакс будет соответствовать позиции скролла. В более старых версиях Mobile Safari события прокрутки фактически срабатывали в конце прокрутки, что сделало невозможным реализацию эффекта параллакса на основе событий прокрутки. В более поздних версиях анимация отображается во время прокрутки, но, как и в Chrome, по принципу «лучшее из возможного». Если основной поток занят какой-либо другой работой, события прокрутки не будут выполняться немедленно, что означает, что эффект параллакса будет потерян.

Плохо: обновление background-position

Другая ситуация, которой мы хотели бы избежать — это отрисовка на каждом кадре. Многие решения пытаются изменить положение фона, чтобы обеспечить эффект параллакса, тем самым заставляя браузер перерисовывать затронутые части страницы при прокрутке. Такое решение может быть достаточно дорогостоящим, чтобы значительно затормозить анимацию.

Если мы хотим реализовать эффект параллакса, мы должны применить решение на основе аппаратно ускоренных свойств (на данный момент это transform и opacity), которые не зависят от событий прокрутки.

CSS в 3D

И Скотт Келлум, и Кит Кларк проделали значительную работу в области использования CSS 3D для реализации эффекта параллакса. Техника, которую они эффективно используют, такова:

  • Установить для контейнера свойство overflow-y: scroll (и возможно overflow-x: hidden).
  • Для этого же элемента применить значение perspective, а свойство perspective-origin установить в top left или 0 0.
  • Для дочерних элементов применить перемещение по оси Z и масштабировать их, чтобы обеспечить эффект параллакса, не влияя на их размер на экране.

CSS для этого выглядит так:


.container {
    width: 100%;
    height: 100%;
    overflow-x: hidden;
    overflow-y: scroll;
    perspective: 1px;
    perspective-origin: 0 0;
}

.parallax-child {
    transform-origin: 0 0;
    transform: translateZ(-2px) scale(3);
}

Эти правила могут быть применены к фрагменту кода:


<div class="container”>
    <div class="parallax-child”></div>
</div>

Настройка масштаба для перспективы

Перемещение элемента на задний план делает его меньше пропорционально значению перспективы. Рассчитать, насколько нужно масштабировать элемент можно по формуле: (perspective — distance) / perspective. Поскольку мы хотим, чтобы элемент был параллаксным, но сохранил исходный размер, мы должны масштабировать его таким образом.

В случае, показанном выше, перспектива равна 1px, а расстояние Z для parallax-child равно −2px. Это означает, что элемент должен быть увеличен до 3x. Это вы можете видеть в коде: scale(3).

Для любого элемента, не имеющего значения translateZ, вы можете заменить значение расстояния на ноль. Это означает, что масштаб будет равен (perspective — 0) / perspective, т.е. 1. Этот элемент не нужно масштабировать ни вверх, ни вниз. Очень удобно, правда.

Как работает этот подход

Важно понять, почему этот подход работает, т.к. скоро мы будем использовать эти знания. Прокрутка фактически является преобразованием, поэтому ее можно аппаратно ускорить; в основном это связано с перемещением слоев с помощью GPU. В обычном скроллинге без перспективы, прокрутка происходит в отношении 1:1, если сравнивать прокрутку контейнера и дочерних элементов. Если вы прокрутите элемент вниз на 300px, элементы переместятся также на 300px.

Применение значения перспективы вносит рассинхронизацию в этот процесс; это значение меняет матрицы, лежащие в основе преобразования прокрутки. Теперь прокрутка на 300px может перемещать дочерние элементы, например, на 150px, в зависимости от выбранной перспективы и значений translateZ. Если значение translateZ элемента равно 0, он будет прокручиваться в отношении 1:1 (как это было раньше), но дочерний элемент, сдвинутый в направлении Z от точки перспективы, будет прокручиваться с другой скоростью! Чистый результат: эффект параллакса. И, что очень важно, это автоматически обрабатывается как часть внутреннего устройства прокрутки браузера, что означает, что нет необходимости слушать события прокрутки или менять положение фона.

Ложка дегтя: Mobile Safari

Есть одно важное предостережение, касающееся сохранения 3D-эффектов для дочерних элементов. Если добавить элементы в иерархию между элементом с перспективой и его параллаксными детьми, 3D перспектива «сплющится», а это означает, что эффект будет потерян.

Есть одно важное предостережение, касающееся сохранения 3D-эффектов для дочерних элементов. Если добавить элементы в иерархию между элементом с перспективой и его параллаксными детьми, 3D перспектива «сплющится», а это означает, что эффект будет потерян.


<div class="container”>
  <div class="parallax-container”>
    <div class="parallax-child”></div>
  </div>
</div>

В коде выше добавился элемент .parallax-container, это фактически делает перспективу плоской и эффект параллакса будет потерян. Решение в большинстве случаев довольно просто: вы добавляете правило transform: preserve-3d к элементу, заставляя его тем самым распространять дальше по дереву любые 3D-эффекты (например, наше значение перспективы), которые были применены.


.parallax-container {
  transform-style: preserve-3d;
}

В случае с Mobile Safari, однако, все немного запутанно. Применение к контейнеру правила overflow-y: scroll технически работает, но за счет переброски прокручиваемых элементов. Решение заключается в добавлении -webkit-overflow-scrolling: touch, но это также сгладит перспективу, и мы не получим никакого параллакса.

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

position: sticky придет на помощь!

В описанной выше ситуации на помощь приходит свойство position: sticky, которое при скролле позволяет элементу «прилипнуть» к верхнему краю вьюпорта или родительского элемента. Спецификация довольно обширна, но есть в ней один полезный маленький камушек:

Блок с position: sticky позиционируется аналогично блоку с position: relative, но смещение вычисляется относительно ближайшего предка с полосой прокрутки или относительно вьюпорта, если ни один из предков не имеет полосы прокрутки. — CSS Positioned Layout Module Level 3

На первый взгляд это может показаться малозначительным, но ключевым моментом в этом предложении является то, как именно вычисляется липкость элемента: «смещение вычисляется по отношению к ближайшему предку с полосой прокрутки». Другими словами, расстояние для перемещения липкого элемента (пока он не станет прикрепленным к другому элементу или к вьюпорту) вычисляется до применения любых других преобразований, а не после. Это означает, что, подобно примеру ранее, если смещение было рассчитано на 300 пикселей, есть возможность использовать перспективу (или любое другое преобразование), чтобы манипулировать этим значением смещения в 300 пикселей, прежде чем оно будет применено к любым липким элементам.

Применение свойства position: -webkit-sticky к параллаксным элементам фактически нейтрализует действие свойства -webkit-overflow-scrolling: touch. Это гарантирует, что положение параллаксного элемента рассчитывается относительно ближайшего предка с полосой прокрутки, в данном случае это элемент .container. Теперь, подобно тому, как мы делали ранее, можно применить к элементу .parallax-container значение перспективы, которое изменит рассчитанное ранее смещение и создаст эффект параллакса.


<div class="container”>
  <div class="parallax-container”>
    <div class="parallax-child”></div>
  </div>
</div>

.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

Это восстанавливает эффект параллакса для Mobile Safari, что является отличной новостью!

Оговорки для липкого позиционирования

Есть одна оговорка: position: sticky изменяет механизм параллакса. Липкое позиционирование пытается, скажем, прикрепить элемент к прокручивающемуся контейнеру, тогда как «не-липкое» позиционирование этого не делает. Это означает, что параллакс с липким позиционированием оказывается противоположным:

  • С position: sticky чем ближе элемент к z=0, тем меньше он движется.
  • Без position: sticky чем ближе элемент к z=0, тем больше он движется.

Если это кажется абстрактным, посмотрите демо Роберта Флэка, которое показывает, как по-разному ведут себя элементы с и без липкого позиционирования. Чтобы увидеть разницу, вам понадобится Chrome Canary (на момент написания статьи версия 56) или Safari.

Демо Роберта Флэка, показывающее, как position: sticky влияет на эффект параллакса.

Различные ошибки и обходные пути

Как всегда, в этом решении есть острые углы, которые необходимо сгладить:

  • Поддержка липкого позиционирования неполная. Поддерживается в Chrome, в Edge поддержка отсутствует полностью, в Firefox есть ошибки при комбинировании position: sticky и перспективы. В таких случаях стоит добавлять position: sticky (версию с префиксом -webkit), когда это необходимо, только для Mobile Safari.
  • Эффект не «просто работает» в Edge. Edge пытается обработать прокрутку на уровне OC, что в целом хорошо, но в данном случае не позволяет обнаружить изменение перспективы во время прокрутки. Чтобы исправить это, вы можете добавить элемент с position: fixed, это переключит Edge на прокрутку без использования ОС и гарантирует учет изменений перспективы.
  • «Содержание страницы стало просто огромным!». Многие браузеры учитывают масштаб при определении размера страницы, но, к сожалению, Chrome и Safari не учитывают перспективу. При масштабе — скажем — 3x, примененном к элементу, вы можете хорошо видеть полосы прокрутки и т.п., даже если элемент находится в масштабе 1x после применения перспективы. Можно обойти эту проблему, масштабируя элементы из нижнего правого угла (transform-origin: bottom right). Это работает, потому что заставляет элементы расти в «отрицательную область».

Заключение

Параллакс — забавный эффект при вдумчивом использовании. Как вы могли убедиться, существует производительный, кросс-браузерный и связанный со прокруткой способ его реализации. Поскольку для достижения желаемого эффекта требуется немного математики и небольшое количества шаблонов, мы обернули это решение в небольшую вспомогательную библиотеку и пример, которые вы можете найти в нашем репозитории UI Element Samples.

Поиграйте, и дайте нам знать, что получится.