Параллаксин'

Введение

В последнее время в моде сайты с параллаксом, просто взгляните на эти:

Если вы с ними не знакомы, это сайты, на которых визуальная структура страницы меняется при прокрутке. Обычно элементы на странице масштабируются, вращаются или перемещаются пропорционально положению прокрутки на странице.

Демонстрационная страница параллакса
Наша демо-страница с эффектом параллакса

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

Разумно обобщить сайт с параллаксом следующим образом:

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

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

Итак, вопрос в том, создаете ли вы сайт с параллаксной прокруткой, вынуждены ли вы дорогостоящие перерисовки или есть альтернативные подходы, которые вы можете использовать для максимизации производительности? Давайте посмотрим на наши варианты.

Вариант 1. Используйте элементы DOM и абсолютные позиции.

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

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

Chrome DevTools без отстраненных событий прокрутки.
Инструменты разработчика, показывающие большие отрисовки и несколько макетов, активируемых событиями, в одном кадре.

Важно иметь в виду, что для достижения 60 кадров в секунду (что соответствует типичной частоте обновления монитора 60 Гц) у нас есть чуть более 16 мс, чтобы все сделать. В этой первой версии мы выполняем визуальные обновления каждый раз, когда получаем событие прокрутки, но, как мы обсуждали в предыдущих статьях о более компактных и грубых анимациях с помощью requestAnimationFrame и производительности прокрутки , это не совпадает с графиком обновлений браузера. и поэтому мы либо пропускаем кадры, либо выполняем слишком много работы внутри каждого из них. Это может легко привести к тому, что ваш сайт станет неестественным и неестественным, что приведет к разочарованию пользователей и несчастным котятам.

Давайте переместим код обновления из события прокрутки в обратный вызов requestAnimationFrame и просто зафиксируем значение прокрутки в обратном вызове события прокрутки.

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

Chrome DevTools с исправленными событиями прокрутки.
Инструменты разработчика, показывающие большие отрисовки и несколько макетов, активируемых событиями, в одном кадре.

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

Основная проблема этого подхода, независимо от того, requestAnimationFrame или нет, заключается в том, что по сути у нас есть один слой для всей страницы, и, перемещая эти визуальные элементы, нам требуются большие (и дорогостоящие) перерисовки. Обычно прорисовка — это блокирующая операция (хотя ситуация меняется ), а это означает, что браузер не может выполнять какую-либо другую работу, и мы часто превышаем бюджет нашего кадра в 16 мс, и все остается нестабильным.

Вариант 2. Используйте элементы DOM и 3D-преобразования.

Вместо использования абсолютных позиций мы можем использовать другой подход — применить к элементам 3D-преобразования. В этой ситуации мы видим, что элементам с примененными 3D-преобразованиями присваивается новый слой для каждого элемента, а в браузерах WebKit это часто также приводит к переключению на аппаратный компоновщик. В варианте 1, напротив, у нас был один большой слой для страницы, который нужно было перерисовывать, когда что-то менялось, а вся прорисовка и компоновка выполнялись центральным процессором.

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

Часто люди просто используют -webkit-transform: translateZ(0); взломайте и увидите волшебное улучшение производительности, и хотя сегодня это работает, есть проблемы:

  1. Он не кроссбраузерен.
  2. Это заставляет браузер работать над созданием нового слоя для каждого преобразованного элемента. Большое количество слоев может стать причиной других узких мест в производительности, поэтому используйте их экономно!
  3. Он отключен для некоторых портов WebKit (четвертый пункт снизу!).

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

Наконец, вам следует избегать рисования везде, где это возможно, и просто перемещать существующие элементы по странице. Например, на сайтах с параллаксом типичным подходом является использование элементов div с фиксированной высотой и изменение их положения фона для достижения нужного эффекта. К сожалению, это означает, что элемент необходимо перерисовывать при каждом проходе, что может негативно сказаться на производительности. Вместо этого вам следует, если можете, создать элемент (обернуть его внутри div с overflow: hidden если необходимо) и вместо этого просто перевести его.

Вариант 3. Используйте холст с фиксированным положением или WebGL.

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

  • Нам больше не требуется столько работы композитора, поскольку у нас есть только один элемент — холст.
  • Фактически мы имеем дело с одним растровым изображением с аппаратным ускорением .
  • API Canvas2D отлично подходит для тех преобразований, которые мы хотим выполнить, а это означает, что разработка и обслуживание становятся более управляемыми.

Использование элемента холста дает нам новый слой, но это всего лишь один слой, тогда как в варианте 2 нам фактически давался новый слой для каждого элемента с примененным 3D-преобразованием, поэтому у нас увеличивается рабочая нагрузка по объединению всех этих слоев. Это также наиболее совместимое решение на сегодняшний день с учетом различных реализаций преобразований в разных браузерах.


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

Этот подход действительно работает, когда вы имеете дело с большими изображениями (или другими элементами, которые можно легко записать на холст), и, конечно, работа с большими блоками текста будет более сложной задачей, но в зависимости от вашего сайта это может оказаться наиболее подходящее решение. Если вам приходится иметь дело с текстом на холсте, вам придется использовать метод API fillText , но это происходит за счет доступности (вы только что растрировали текст в растровое изображение!), и теперь вам придется иметь дело с переносом строк и еще куча других вопросов. Если вы можете этого избежать, вам действительно следует это сделать, и вам, вероятно, будет лучше использовать описанный выше подход к преобразованиям.

Поскольку мы зашли в этом настолько далеко, насколько это возможно, нет никаких оснований предполагать, что работа с параллаксом должна выполняться внутри элемента холста. Если браузер поддерживает это, мы можем использовать WebGL. Ключевым моментом здесь является то, что WebGL имеет самый прямой путь из всех API к видеокарте и, как таковой, является вашим наиболее вероятным кандидатом на достижение 60 кадров в секунду, особенно если эффекты сайта сложны.

Ваша немедленная реакция может заключаться в том, что WebGL является излишним или что он не является повсеместным с точки зрения поддержки, но если вы используете что-то вроде Three.js , вы всегда можете вернуться к использованию элемента холста, и ваш код абстрагируется в согласованном виде. и дружелюбная манера. Все, что нам нужно сделать, это использовать Modernizr , чтобы проверить наличие соответствующей поддержки API:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

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

Выбор ваш

Основной причиной, по которой разработчики по умолчанию используют абсолютно позиционированные элементы, а не любой другой вариант, может быть просто повсеместная поддержка. В какой-то степени это иллюзорно, поскольку старые браузеры, на которые нацелена атака, скорее всего, будут обеспечивать крайне плохой рендеринг. Даже в современных браузерах использование абсолютно позиционированных элементов не обязательно приводит к хорошей производительности!

Преобразования, особенно трехмерные, дают вам возможность работать напрямую с элементами DOM и достигать стабильной частоты кадров. Ключ к успеху здесь — избегать рисования там, где это возможно, а просто пытаться перемещать элементы. Имейте в виду, что способ создания слоев в браузерах WebKit не обязательно соответствует другим браузерным движкам, поэтому обязательно протестируйте его, прежде чем приступать к использованию этого решения.

Если вы нацелены только на верхний уровень браузеров и можете отображать сайт с помощью холстов, это может быть лучшим вариантом для вас. Конечно, если вы будете использовать Three.js, вы сможете очень легко переключаться между рендерерами в зависимости от необходимой вам поддержки.

Заключение

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

И, как всегда, какой бы подход вы ни попробовали: не угадывайте, а протестируйте .