Paralaje de alto rendimiento

Roberto López
Robert Flack

Te guste o no, la paralaje llegó para quedarse. Cuando se usa con criterio, puede agregar profundidad y sutileza a una app web. Sin embargo, el problema es que implementar el paralaje de manera eficaz puede ser desafiante. 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 CSS 3D 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 una solución directa, ve al repositorio de GitHub de muestras de elementos de la IU y obtén el JS auxiliar de Paralaje. Puedes ver una demostración en vivo de la barra de desplazamiento paralaje en el repositorio de GitHub.

Paralajeadores 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.

Incorrecto: Se usan eventos de desplazamiento

El requisito clave del paralaje es que debe estar vinculado con desplazamientos. Por cada cambio en la posición de desplazamiento de la página, la posición del elemento de paralaje debe actualizarse. Aunque parece simple, un mecanismo importante de los navegadores modernos es su capacidad para trabajar de manera 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 según los eventos de desplazamiento: JavaScript no garantiza que el paralaje se ajuste a 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 hacía imposible crear un efecto de desplazamiento basado en JavaScript. Las versiones más recientes ofrecen eventos de desplazamiento durante la animación, pero, al igual que en Chrome, según el "mejor esfuerzo". Si el subproceso principal está ocupado con otra tarea, 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 queremos 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 al desplazarse, lo que puede ser lo suficientemente costoso como para bloquear significativamente la animación.

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

CSS en 3D

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

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

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

Lo que supone un fragmento de HTML como este:

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

Ajusta la escala para mejorar la perspectiva

Si se devuelve el elemento secundario, este se volverá más pequeño en proporción al valor de la perspectiva. Para calcular cuánto se debe escalar verticalmente, puedes usar la siguiente ecuación: (perspectiva - distancia) / perspectiva. Como lo más probable es que queramos que el elemento de paralaje se paralaje, pero aparezca en el tamaño que creamos, debería escalarse verticalmente de esta manera, en lugar de dejarse como está.

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

En el caso de cualquier contenido que no tenga un valor translateZ aplicado, puedes sustituir un valor de cero. Esto significa que la escala es (perspectiva - 0) / perspectiva, que sale a un valor de 1, lo que significa que no se escaló ni aumentó ni disminuyó. Es bastante útil.

Cómo funciona este enfoque

Es importante tener claro por qué esto funciona, ya que usaremos ese conocimiento en breve. En la práctica, el desplazamiento es 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 ocurre de manera 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 en la misma cantidad: 300px.

Sin embargo, aplicar un valor de perspectiva al elemento de desplazamiento afecta este proceso; cambia las matrices que respaldan la transformación de desplazamiento. Ahora, un desplazamiento de 300 px solo puede mover los elementos secundarios 150 px, según los valores de perspective y translateZ que elijas. Si un elemento tiene un valor translateZ de 0, se desplazará a la velocidad 1:1 (como antes), pero un elemento secundario que se envíe en Z hacia fuera del origen de perspectiva se desplazará a una velocidad diferente. Resultado neto: movimiento de paralaje. Y, lo que es más importante, esto se controla automáticamente como parte de la maquinaria de desplazamiento interno del navegador, lo que significa que no es necesario escuchar eventos scroll ni cambiar background-position.

Una mosca en el ungüento: Mobile Safari

Hay advertencias sobre cada efecto, y uno 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 3D está "plana", 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, y perdemos el efecto de paralaje. La solución, en la mayoría de los casos, es bastante sencilla: agregas transform-style: preserve-3d al elemento, lo que hace que propague cualquier efecto 3D (como nuestro valor de perspectiva) que se aplicó más arriba en el árbol.

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

Sin embargo, en el caso de Mobile Safari, las cosas son un poco más complicadas. Técnicamente, aplicar overflow-y: scroll al elemento de contenedor funciona, pero a costa de poder arrastrar el elemento de desplazamiento. La solución es agregar -webkit-overflow-scrolling: touch, pero también se compactará perspective y no se obtendría 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, hay ayuda en forma de position: sticky, que permite que los elementos se “peguen” en la parte superior del viewport o 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:

Puede parecer que esto no signifique demasiado a primera vista, pero un punto clave en esa oración es cuando se refiere a cómo, exactamente, la permanencia de un elemento se calcula: "el desplazamiento se calcula con referencia al principal más cercano con un cuadro de desplazamiento". En otras palabras, la distancia para mover el elemento persistente (para que aparezca adjunto 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 de 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.

Cuando se aplica position: -webkit-sticky al elemento de paralaje, se puede "revertir" el efecto de compactación de -webkit-overflow-scrolling: touch de forma efectiva. 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 en Mobile Safari, lo cual 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 pegar el elemento en el contenedor de desplazamiento, mientras que una versión no fija no lo hace. Esto significa que el paralaje con el valor fijo es lo contrario al que no tiene lo siguiente:

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

Si todo parece algo abstracto, mira esta demostración de Robert Flack, que demuestra cómo los elementos se comportan de manera diferente con y sin el posicionamiento fijo. Para ver la diferencia, necesitas Chrome Canary (versión 56 al momento de redactar 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.

Varios errores y soluciones

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

  • La compatibilidad permanente es incoherente. La compatibilidad aún se está implementando en Chrome, Edge no es compatible por completo y Firefox tiene errores de pintura cuando el efecto persistente se combina con las transformaciones de perspectiva. En esos casos, vale la pena agregar un pequeño 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 "simplemente funciona" en Edge. Edge intenta controlar el desplazamiento a nivel del SO, lo que 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 es 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. Entonces, si se aplica, por ejemplo, una escala de 3x a un elemento, es posible que veas barras de desplazamiento y elementos similares, incluso si el elemento está en 1x después de que se aplicó perspective. Es posible solucionar este problema escalando los elementos desde la esquina inferior derecha (con transform-origin: bottom right), lo que funciona porque hace 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 el contenido de la región negativa ni desplazarte a él.

Conclusión

El paralaje es un efecto divertido cuando se usa con cuidado. Como puedes ver, es posible implementarla de una manera que tenga un buen rendimiento, esté vinculada con desplazamiento y sea en varios navegadores. Dado que requiere algunas negociaciones matemáticas y una pequeña cantidad de código estándar para lograr el efecto deseado, reunimos una pequeña biblioteca de ayuda y un ejemplo, que puedes encontrar en nuestro repositorio de GitHub de muestras de elementos de IU.

Juega y cuéntanos cómo te va.