Paralaje de alto rendimiento

Te guste o no, el efecto parallax llegó para quedarse. Cuando se usa con prudencia, puede agregar profundidad y sutileza a una app web. Sin embargo, el problema es que implementar el paralaje de manera eficiente puede ser un desafío. En este artículo, analizaremos una solución que tiene un buen rendimiento y, lo que es igual de importante, funciona en varios navegadores.

Ilustración de paralaje.

Resumen

  • No uses eventos de desplazamiento ni background-position para crear animaciones de paralaje.
  • Usa transformaciones 3D de CSS para crear un efecto de paralaje más preciso.
  • En el caso de Mobile Safari, usa position: sticky para asegurarte de que se propague el efecto de paralaje.

Si quieres la solución integrada, ve al repositorio de GitHub de Samples de elementos de IU y obtén el ayudante de JS de Parallax. Puedes ver una demostración en vivo del control deslizante de paralaje en el repositorio de GitHub.

Problemas de paralaje

Para empezar, analicemos dos formas comunes de lograr un efecto de paralaje y, en particular, por qué no son adecuadas para nuestros fines.

No se recomienda: usar eventos de desplazamiento

El requisito clave de la paralaje es que debe estar acoplada al desplazamiento. Para cada cambio en la posición de desplazamiento de la página, la posición del elemento de paralaje debe actualizarse. Si bien eso suena simple, un mecanismo importante de los navegadores modernos es su capacidad de trabajar de forma asíncrona. Esto se aplica, en nuestro caso particular, a los eventos de desplazamiento. En la mayoría de los navegadores, los eventos de desplazamiento se entregan como “mejor esfuerzo” y no se garantiza que se entreguen en cada fotograma de la animación de desplazamiento.

Esta información importante nos indica por qué debemos evitar una solución basada en JavaScript que mueva elementos en función de eventos de desplazamiento: JavaScript no garantiza que el efecto de paralaje se mantenga a la par de la posición de desplazamiento de la página. En versiones anteriores de Mobile Safari, los eventos de desplazamiento se entregaban al final del desplazamiento, lo que imposibilitaba crear un efecto de desplazamiento basado en JavaScript. Las versiones más recientes envían eventos de desplazamiento durante la animación, pero, al igual que Chrome, de forma "del mejor esfuerzo". Si el subproceso principal está ocupado con algún otro trabajo, los eventos de desplazamiento no se entregarán de inmediato, lo que significa que se perderá el efecto de paralaje.

Error: Actualización de background-position

Otra situación que nos gustaría evitar es pintar en cada fotograma. Muchas soluciones intentan cambiar background-position para proporcionar el aspecto de paralaje, lo que hace que el navegador vuelva a pintar las partes afectadas de la página cuando se desplaza, y eso puede ser lo suficientemente costoso como para generar una gran inestabilidad en la animación.

Si queremos cumplir con la promesa del movimiento de paralaje, necesitamos algo que se pueda aplicar como una propiedad acelerada (que hoy significa apegarse a las transformaciones y la opacidad) y que no dependa de eventos de desplazamiento.

CSS en 3D

Tanto Scott Kellum como Keith Clark realizaron un trabajo significativo en el área del uso de CSS 3D para lograr el movimiento de paralaje, y la técnica que usan es, en efecto, la siguiente:

  • Configura un elemento contenedor para que se desplace con overflow-y: scroll (y probablemente overflow-x: hidden).
  • A ese mismo elemento, aplícale un valor perspective y un perspective-origin configurado en top left o 0 0.
  • A los elementos secundarios de ese elemento, aplícales una traslación en Z y vuelve a escalarlos para proporcionar un movimiento de paralaje sin afectar su tamaño en la pantalla.

El CSS para este enfoque se ve de la siguiente manera:

.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);
}

Que supone un fragmento de HTML como este:

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

Cómo ajustar la escala para la perspectiva

Si alejas el elemento secundario, este se hará más pequeño de manera proporcional al valor de perspectiva. Puedes calcular cuánto se deberá escalar con esta ecuación: (perspectiva - distancia) / perspectiva. Dado que lo más probable es que deseemos que el elemento de paralaje tenga paralaje, pero que aparezca en el tamaño en el que lo creamos, debería aumentarse de esta manera, en lugar de dejarlo como está.

En el caso del código anterior, la perspectiva es 1px y la distancia en Z de parallax-child es -2px. Esto significa que el elemento deberá escalarse 3 veces, que puedes ver que es el valor conectado al código: scale(3).

Para el contenido que no tenga aplicado un valor translateZ, puedes reemplazarlo por un valor de cero. Esto significa que la escala es (perspectiva - 0) / perspectiva, que se redondea a un valor de 1, lo que significa que no se ajustó ningún valor hacia arriba ni hacia abajo. Es muy útil.

Cómo funciona este enfoque

Es importante tener claro por qué funciona, ya que usaremos ese conocimiento en breve. El desplazamiento es, en realidad, una transformación, por lo que se puede acelerar. En su mayoría, implica mover capas con la GPU. En un desplazamiento típico, que es uno sin ninguna noción de perspectiva, el desplazamiento se produce de forma 1:1 cuando se compara el elemento de desplazamiento y sus elementos secundarios. Si desplazas un elemento hacia abajo en 300px, sus elementos secundarios se transforman hacia arriba en la misma cantidad: 300px.

Sin embargo, aplicar un valor de perspectiva al elemento de desplazamiento altera este proceso, ya que cambia las matrices que sustentan la transformación de desplazamiento. Ahora, un desplazamiento de 300 px solo puede mover los elementos secundarios en 150 px, según los valores de perspective y translateZ que hayas elegido. Si un elemento tiene un valor de translateZ de 0, se desplazará a 1:1 (como solía hacerlo), pero un elemento secundario que se empuje en Z lejos del origen de la perspectiva se desplazará a una velocidad diferente. Resultado neto: movimiento de paralaje. Y, lo que es muy importante, esto se controla automáticamente como parte del mecanismo de desplazamiento interno del navegador, lo que significa que no es necesario escuchar eventos scroll ni cambiar background-position.

Un inconveniente: Safari para dispositivos móviles

Hay salvedades para cada efecto, y una importante para las transformaciones es la preservación de los efectos 3D en los elementos secundarios. Si hay elementos en la jerarquía entre el elemento con una perspectiva y sus elementos secundarios con paralaje, la perspectiva en 3D se “aplana”, lo que significa que se pierde el efecto.

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

En el código HTML anterior, .parallax-container es nuevo y aplanará de manera efectiva el valor perspective, por lo que perderemos el efecto de paralaje. En la mayoría de los casos, la solución es bastante sencilla: agregas transform-style: preserve-3d al elemento, lo que hace que se propaguen los efectos 3D (como nuestro valor de perspectiva) que se aplicaron más arriba en el árbol.

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

Sin embargo, en el caso de Safari para dispositivos móviles, la situación es un poco más complicada. Aplicar overflow-y: scroll al elemento del contenedor funciona técnicamente, pero a costa de poder lanzar el elemento de desplazamiento. La solución es agregar -webkit-overflow-scrolling: touch, pero también aplanará el perspective y no obtendremos ningún paralaje.

Desde el punto de vista de la mejora progresiva, esto probablemente no sea un problema demasiado grande. Si no podemos usar el efecto de paralaje en todas las situaciones, nuestra app seguirá funcionando, pero sería bueno encontrar una solución alternativa.

position: sticky al rescate

De hecho, hay cierta ayuda en forma de position: sticky, que existe para permitir que los elementos se “fijen” en la parte superior del viewport o en un elemento superior determinado durante el desplazamiento. La especificación, como la mayoría de ellas, es bastante extensa, pero contiene una pequeña joya útil:

A primera vista, esto puede no parecer muy importante, pero un punto clave de esa oración es cuando se refiere a cómo se calcula exactamente la fijación de un elemento: “el desplazamiento se calcula en función del ancestro más cercano con un cuadro de desplazamiento”. En otras palabras, la distancia para mover el elemento fijo (para que aparezca unido a otro elemento o al viewport) se calcula antes de que se apliquen otras transformaciones, no después. Esto significa que, al igual que en el ejemplo de desplazamiento anterior, si el desplazamiento se calculó en 300 px, hay una nueva oportunidad para usar perspectivas (o cualquier otra transformación) para manipular ese valor de desplazamiento de 300 px antes de que se aplique a cualquier elemento fijo.

Si aplicamos position: -webkit-sticky al elemento de paralaje, podemos “revertir” de manera eficaz el efecto de aplanamiento de -webkit-overflow-scrolling: touch. Esto garantiza que el elemento de paralaje haga referencia al ancestro más cercano con un cuadro de desplazamiento, que en este caso es .container. Luego, de manera similar a antes, .parallax-container aplica un valor perspective, que cambia el desplazamiento calculado y crea un efecto de paralaje.

<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);
}

Esto restablece el efecto de paralaje para Mobile Safari, lo que es una excelente noticia.

Advertencias sobre el posicionamiento fijo

Sin embargo, hay una diferencia: position: sticky altera la mecánica de paralaje. El posicionamiento fijo intenta fijar el elemento al contenedor de desplazamiento, mientras que una versión no fija no lo hace. Esto significa que la paralaje con elementos fijos termina siendo la inversa de la que no tiene:

  • Con position: sticky, cuanto más cerca esté el elemento de z=0, menos se moverá.
  • Sin position: sticky, cuanto más cerca esté el elemento de z=0, más se moverá.

Si todo esto te parece un poco abstracto, mira esta demostración de Robert Flack, en la que se muestra cómo los elementos se comportan de manera diferente con y sin posicionamiento fijo. Para ver la diferencia, necesitas Chrome Canary (que es la versión 56 en el momento de escribir este artículo) o Safari.

Captura de pantalla de la perspectiva de paralaje

Demostración de Robert Flack que muestra cómo position: sticky afecta el desplazamiento de paralaje.

Diversos errores y soluciones

Sin embargo, como con cualquier cosa, todavía hay irregularidades que se deben suavizar:

  • La compatibilidad con la función de fijar elementos no es coherente. La compatibilidad aún se está implementando en Chrome, Edge no la admite en absoluto y Firefox tiene errores de pintura cuando se combina la fijación con transformaciones de perspectiva. En esos casos, vale la pena agregar un poco de código para agregar solo position: sticky (la versión con prefijo -webkit-) cuando sea necesario, que es solo para Safari para dispositivos móviles.
  • El efecto no “funciona” en Edge. Edge intenta controlar el desplazamiento a nivel del SO, lo que, en general, es algo bueno, pero, en este caso, evita que detecte los cambios de perspectiva durante el desplazamiento. Para solucionar este problema, puedes agregar un elemento de posición fija, ya que parece cambiar Edge a un método de desplazamiento que no es del SO y garantiza que se tengan en cuenta los cambios de perspectiva.
  • “El contenido de la página se hizo enorme”. Muchos navegadores tienen en cuenta la escala cuando deciden qué tan grande es el contenido de la página, pero, lamentablemente, Chrome y Safari no tienen en cuenta la perspectiva. Por lo tanto, si hay, por ejemplo, una escala de 3x aplicada a un elemento, es posible que veas barras de desplazamiento y elementos similares, incluso si el elemento está en 1x después de aplicar perspective. Para solucionar este problema, puedes escalar los elementos desde la esquina inferior derecha (con transform-origin: bottom right), lo que funciona porque hará que los elementos de gran tamaño crezcan en la "región negativa" (por lo general, la parte superior izquierda) del área desplazable. Las regiones desplazables nunca te permiten ver ni desplazarte hasta el contenido de la región negativa.

Conclusión

El efecto de paralaje es divertido cuando se usa con cuidado. Como puedes ver, es posible implementarlo de una manera que sea eficiente, acoplada al desplazamiento y multinavegador. Dado que requiere un poco de cálculo matemático y una pequeña cantidad de texto de plantilla para obtener el efecto deseado, creamos una pequeña biblioteca de ayuda y un ejemplo, que puedes encontrar en nuestro repositorio de GitHub de muestras de elementos de IU.

Pruébala y cuéntanos cómo te fue.