Paralaje de alto rendimiento

Te guste o no, el paralaje llegó para quedarse. Cuando se usa con cuidado, puede agregar profundidad y sutileza a una app web. Sin embargo, el problema es que implementar la paralaje de manera eficaz puede ser desafiante. En este artículo, analizaremos una solución que tiene buen rendimiento y, lo que es más importante, que 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 en CSS para crear un efecto de paralaje más preciso.
  • En Mobile Safari, usa position: sticky para asegurarte de que se propague el efecto de paralaje.

Si quieres la solución directa, ve al repositorio de GitHub de muestras de elementos de la IU y toma el JS auxiliar de Paralaje. Puedes ver una demostración en vivo de la barra de desplazamiento paralaje en el repositorio de GitHub.

Paralajeres de problemas

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

Malo: uso de eventos de desplazamiento

El requisito clave del paralaje es que debe estar vinculado con desplazamiento. Para cada cambio en la posición de desplazamiento de la página, la posición del elemento de paralaje debe actualizarse. Aunque parezca sencillo, 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 ofrecen 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 mueve elementos en función de los eventos de desplazamiento: JavaScript no garantiza que el paralaje se mantenga al nivel de la posición de desplazamiento de la página. En las versiones anteriores de Mobile Safari, los eventos de desplazamiento se entregaban al final del desplazamiento, lo que hacía imposible generar un efecto de desplazamiento basado en JavaScript. Las versiones más recientes entregan eventos de desplazamiento durante la animación, pero, al igual que Chrome, en función del "mejor esfuerzo". Si el subproceso principal está ocupado con otro trabajo, los eventos de desplazamiento no se entregarán de inmediato, lo que significa que se perderá el efecto de paralaje.

Incorrecto: actualizando background-position

Otra situación que nos gustaría evitar es pintar en cada marco. 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 durante el desplazamiento, lo que puede ser lo suficientemente costoso como para bloquear la animación de manera significativa.

Si queremos cumplir con la promesa del movimiento con paralaje, queremos 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 con paralaje, y la técnica que usan es la siguiente:

  • Configura un elemento contenedor para desplazarte con overflow-y: scroll (y probablemente overflow-x: hidden).
  • A ese mismo elemento, aplica un valor perspective y un perspective-origin establecido en top left o 0 0.
  • Aplica una traslación en Z a los elementos secundarios de ese elemento y vuelve a escalarlos para proporcionar movimiento con paralaje sin afectar su tamaño en pantalla.

El CSS para este enfoque se ve así:

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

Lo que supone un fragmento de HTML como el siguiente:

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

Ajustar la escala para la perspectiva

Enviar el elemento secundario hacia atrás hará que se vuelva más proporcional al valor de perspectiva. Puedes calcular cuánto se necesitará escalar verticalmente con esta ecuación: (perspectiva - distancia) / perspectiva. Dado que lo más probable es que queramos que el elemento paralaje se paralaje, pero que aparezca en el tamaño que creamos, deberíamos ampliarlo de esta manera, en lugar de dejarse tal como está.

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

Puedes sustituir el contenido al que no se aplicó un valor translateZ. Esto significa que la escala es (perspective - 0) / perspective, que da un valor de 1, lo que significa que se escaló ni hacia arriba ni hacia abajo. Bastante útil, de verdad.

Cómo funciona este enfoque

Es importante que quede claro por qué funciona, ya que usaremos ese conocimiento en breve. El desplazamiento es, en la práctica, una transformación, por lo que se puede acelerar; principalmente implica cambiar las capas con la GPU. En un desplazamiento típico, que no tiene noción de perspectiva, el desplazamiento se produce de manera 1:1 cuando se compara el elemento de desplazamiento con sus elementos secundarios. Si desplazas un elemento hacia abajo en 300px, sus elementos secundarios se transforman en la misma cantidad: 300px.

Sin embargo, aplicar un valor de perspectiva al elemento de desplazamiento desordena este proceso; cambia las matrices que respaldan 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 elijas. Si un elemento tiene un valor de translateZ de 0, se desplazará a 1:1 (como antes), pero un elemento secundario que se aleje en Z desde el origen de la perspectiva se desplazará a una velocidad diferente. Resultado neto: movimiento con paralaje. Y, lo que es más importante, esto se controla automáticamente como parte de la maquinaria de desplazamiento interna del navegador, lo que significa que no es necesario escuchar los eventos de scroll ni cambiar background-position.

Una mosca en el ungüento: Mobile Safari

Cada efecto tiene advertencias, y una importante para las transformaciones es la conservació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 de paralaje, la perspectiva 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 HTML anterior, .parallax-container es nuevo y compactará el valor perspective de manera efectiva, lo que perderá el efecto de paralaje. En la mayoría de los casos, la solución es bastante sencilla: debes agregar transform-style: preserve-3d al elemento, lo que hace que se propague cualquier efecto 3D (como nuestro valor de perspectiva) que se haya aplicado más arriba en el árbol.

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

Sin embargo, en el caso de Mobile Safari, la situación es un poco más complicada. Técnicamente, aplicar overflow-y: scroll al elemento de contenedor funciona, pero a costa de poder deslizar el elemento de desplazamiento. La solución es agregar -webkit-overflow-scrolling: touch, pero también compactará el perspective y no obtendremos ningún paralaje.

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

¡position: sticky al rescate!

De hecho, existe ayuda en forma de position: sticky, que existe para permitir que los elementos se "permanezcan" 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 abundante, pero contiene una pequeña gema útil:

Puede parecer que no signifique mucho a primera vista, pero un punto clave en esa oración es cuando se refiere a cómo se calcula exactamente la permanencia de un elemento: "el desplazamiento se calcula con referencia al elemento principal más cercano con un cuadro de desplazamiento". En otras palabras, la distancia para mover el elemento fijo (para que aparezca adjunto a otro elemento o al viewport) se calcula antes de que se aplique cualquier otra transformación, no después. Esto significa que, al igual que en el ejemplo de desplazamiento anterior, si el desplazamiento se calculó en 300 px, existe 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 efectiva el efecto de compactación de -webkit-overflow-scrolling: touch. Esto garantiza que el elemento de paralaje haga referencia al principal más cercano con un cuadro de desplazamiento, que en este caso es .container. Luego, al igual que 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 de Mobile Safari, lo que representa una excelente noticia en todo momento.

Advertencias sobre el posicionamiento fijo

Sin embargo, existe una diferencia: position: sticky modifica la mecánica del 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 el paralaje con pegajoso termina siendo el inverso del que no tiene lo siguiente:

  • Con position: sticky, cuanto más cerca se encuentra el elemento a z=0, menos se mueve.
  • Sin position: sticky, cuanto más cerca se encuentre el elemento de z=0, más se moverá.

Si todo parece un poco abstracto, mira esta demostración de Robert Flack, que demuestra cómo los elementos se comportan de manera diferente con el posicionamiento fijo y sin él. Para ver la diferencia, necesitas Chrome Canary (que tiene la versión 56 al momento de escribir este documento) o Safari.

Captura de pantalla de perspectiva de paralaje

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

Diversos errores y soluciones alternativas

Sin embargo, como sucede con todo, todavía hay protuberancias que se deben suavizar:

  • La compatibilidad fija no es coherente. Aún se está implementando la compatibilidad en Chrome, Edge no la admite por completo y Firefox tiene errores de pintura cuando lo atractivo se combina con las transformaciones de perspectiva. En esos casos, vale la pena agregar un poco de código para agregar solo position: sticky (la versión con el prefijo -webkit-) cuando sea necesario, que es solo para Mobile Safari.
  • El efecto no funciona simplemente en Edge. Edge intenta controlar el desplazamiento a nivel del SO, lo cual suele ser 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 esto parece cambiar Edge a un método de desplazamiento que no sea del SO y garantiza que tenga en cuenta los cambios de perspectiva.
  • “El contenido de la página se ha hecho enorme”. Muchos navegadores tienen en cuenta la escala a la hora de decidir el tamaño del contenido de la página, pero Chrome y Safari no tienen en cuenta la perspectiva. Por lo tanto, si se aplica, por ejemplo, una escala de 3x a un elemento, es posible que veas barras de desplazamiento y otras similares, incluso si el elemento está en 1x después de que se aplicó perspective. Es posible solucionar este problema si se escalan los elementos desde la esquina inferior derecha (con transform-origin: bottom right), lo que funciona porque hará que los elementos de gran tamaño se conviertan en la "región negativa" (por lo general, la parte superior izquierda) del área desplazable. Estas regiones nunca te permiten ver el contenido en la región negativa ni desplazarte al contenido.

Conclusión

El paralaje es un efecto divertido cuando se usa cuidadosamente. Como puedes ver, es posible implementarlo de manera eficaz, con desplazamiento y para varios navegadores. Dado que requiere una pequeña transformación matemática y una pequeña cantidad de código estándar para obtener el efecto deseado, reunimos una pequeña biblioteca auxiliar y una muestra que puedes encontrar en nuestro repositorio de GitHub de muestras de elementos de IU.

Diviértete y cuéntanos cómo te va.