Paralaje'

Introducción

Los sitios de paralaje son populares recientemente. Echa un vistazo a estos:

Si no estás familiarizado con ellos, son los sitios en los que cambia la estructura visual de la página a medida que te desplazas. Normalmente, los elementos dentro de la escala de la página se escalan, rotan o se mueven proporcionalmente a la posición de desplazamiento en la página.

Una página de paralaje de demostración
Nuestra página de demostración está completa con efecto de paralaje

Si te gusta o no el paralaje de los sitios, es algo que puedes decir, pero con bastante confianza es que son un agujero negro del rendimiento. Esto se debe a que los navegadores tienden a estar optimizados para los casos en que aparezca contenido nuevo en la parte superior o inferior de la pantalla cuando te desplaces (según la dirección de desplazamiento) y, en términos generales, los navegadores funcionan mejor cuando hay muy pocos cambios visuales durante un desplazamiento. Es el caso de un sitio con paralaje, ya que muchas veces los elementos visuales grandes de toda la página cambian, lo que provoca que el navegador vuelva a pintar toda la página.

Es lógico generalizar un sitio con paralaje como el siguiente:

  • Elementos de fondo que, a medida que te desplazas hacia arriba y hacia abajo, cambian su posición, rotación y escala.
  • Contenido de la página, como texto o imágenes más pequeñas, que se desplazan de manera típica de arriba abajo.

Anteriormente, hablamos sobre el rendimiento del desplazamiento y las formas en que puedes mejorar la capacidad de respuesta de tu app. Este artículo se basa en esa base, por lo que puede valer la pena leerlo, si aún no lo hiciste.

Entonces, la pregunta es si estás creando un sitio con desplazamiento con paralaje, ¿tienes restricciones de repeticiones costosas o hay enfoques alternativos que puedes adoptar para maximizar el rendimiento? Veamos nuestras opciones.

Opción 1: Usa elementos del DOM y posiciones absolutas

Este parece ser el enfoque predeterminado que la mayoría de las personas adopta. Hay muchos elementos en la página y, cada vez que se activa un evento de desplazamiento, se realizan varias actualizaciones visuales para transformarlos.

Si inicias Timeline de Herramientas para desarrolladores en el modo de marco y te desplazas, notarás que hay operaciones de pintura costosas en pantalla completa y, si te desplazas mucho, es posible que veas varios eventos de desplazamiento dentro de un solo fotograma, cada uno de los cuales activará el trabajo de diseño.

Herramientas para desarrolladores de Chrome sin eventos de desplazamiento con anulación de rebote.
Herramientas para desarrolladores que muestran pinturas grandes y varios diseños activados por eventos en un solo fotograma.

Lo importante es tener en cuenta que, para alcanzar los 60 FPS (que coinciden con la frecuencia de actualización típica del monitor de 60 Hz), tenemos un poco más de 16 ms para hacer todo. En esta primera versión, realizamos nuestras actualizaciones visuales cada vez que recibimos un evento de desplazamiento. Sin embargo, como ya comentamos en artículos anteriores sobre animaciones más fluidas y más crudas con requestAnimationFrame y rendimiento de desplazamiento, esto no coincide con el programa de actualización del navegador, por lo que perdemos fotogramas o trabajamos demasiado dentro de cada uno. Como resultado, es muy posible que se genere una sensación de irregularidad y poco natural en el sitio, lo que podría llevar a usuarios decepcionados y gatitos descontentos.

Movamos el código de actualización del evento de desplazamiento a una devolución de llamada de requestAnimationFrame y simplemente capturemos el valor de desplazamiento en la devolución de llamada del evento de desplazamiento.

Si repites la prueba de desplazamiento, es posible que notes una leve mejora, aunque no mucho. Esto se debe a que la operación de diseño que activamos mediante el desplazamiento no es tan costosa, pero en otros casos de uso podría serlo. Ahora, al menos, solo realizamos una operación de diseño en cada fotograma.

Herramientas para desarrolladores de Chrome con eventos de desplazamiento con anulación de rebote.
Herramientas para desarrolladores que muestran pinturas grandes y varios diseños activados por eventos en un solo fotograma.

Ahora podemos controlar uno o cien eventos de desplazamiento por fotograma, pero lo más importante es que solo almacenamos el valor más reciente para usarlo cada vez que se ejecuta la devolución de llamada de requestAnimationFrame y realiza nuestras actualizaciones visuales. El punto es que pasaste de intentar forzar actualizaciones visuales cada vez que recibes un evento de desplazamiento a solicitar que el navegador te proporcione una ventana apropiada para realizarlas. ¿No eres dulce?

El problema principal con este enfoque, requestAnimationFrame o no, es que básicamente tenemos una capa para toda la página y, cuando se mueven estos elementos visuales, se deben realizar repeticiones grandes (y costosas). Por lo general, la pintura es una operación de bloqueo (aunque está cambiando), lo que significa que el navegador no puede realizar ningún otro trabajo y, a menudo, superamos el presupuesto de 16 ms de nuestro fotograma, y los bloqueos siguen funcionando.

Opción 2: Usa elementos del DOM y transformaciones 3D

En lugar de usar posiciones absolutas, otro enfoque que podemos adoptar es aplicar transformaciones 3D a los elementos. En esta situación, vemos que a los elementos con las transformaciones 3D aplicadas se les asigna una nueva capa por elemento y, en los navegadores WebKit, esto a menudo también genera un cambio al compositor de hardware. En la opción 1, por el contrario, teníamos una gran capa para la página que debía volver a pintarse cuando algo cambiaba y la CPU manejaba toda la pintura y la composición.

Eso significa que con esta opción las cosas son diferentes: es posible que tengamos una capa para cualquier elemento al que apliquemos una transformación 3D. Si todo lo que hacemos a partir de este punto son más transformaciones en los elementos, no tendremos que volver a pintar la capa, y la GPU se encarga de mover los elementos y componer juntos la página final.

Muchas veces, las personas simplemente usan el truco de -webkit-transform: translateZ(0); y ven mejoras mágicas en el rendimiento. Si bien esto funciona hoy en día, hay algunos problemas:

  1. No es compatible con varios navegadores.
  2. Forzar la mano del navegador creando una nueva capa para cada elemento transformado El uso de muchas capas puede provocar otros cuellos de botella en el rendimiento, así que úsalas con moderación.
  3. Se inhabilitó para algunos puertos de WebKit (cuarta viñeta desde la parte inferior).

Si tomas la ruta de la traducción 3D, ten cuidado, ¡es una solución temporal a tu problema! Idealmente, veríamos características de renderización similares a las de las transformaciones 2D que a las 3D. Los navegadores están progresando a un ritmo fenomenal. Esperamos que antes de que eso sea lo que veremos.

Por último, debes tratar de evitar las pinturas siempre que puedas y simplemente mover los elementos existentes por la página. A modo de ejemplo, un enfoque típico en sitios con paralaje es usar elementos div de altura fija y cambiar la posición de fondo para generar el efecto. Lamentablemente, eso significa que el elemento se debe volver a pintar en cada pase, lo que puede afectar el rendimiento. En su lugar, si es posible, debes crear el elemento (unirlo dentro de un elemento div con overflow: hidden de ser necesario) y simplemente traducirlo.

Opción 3: Usa un lienzo de posición fija o WebGL

La última opción que consideraremos es usar un lienzo de posición fija en la parte posterior de la página en la que dibujaremos las imágenes transformadas. A primera vista, puede que no parezca la solución con mejor rendimiento, pero, en realidad, este enfoque tiene algunos beneficios:

  • Ya no necesitamos tanto trabajo del compositor debido a que solo tenemos un elemento: el lienzo.
  • Trabajamos de forma efectiva con un solo mapa de bits acelerado por hardware.
  • La API de Canvas2D es una excelente opción para el tipo de transformaciones que queremos realizar, lo que significa que el desarrollo y el mantenimiento son más manejables.

El uso de un elemento de lienzo nos da una nueva capa, pero es solo una capa, mientras que en la opción 2, en realidad, se nos proporcionó una capa nueva para todos los elementos con una transformación 3D aplicada, por lo que obtuvimos una mayor carga de trabajo que componía todas esas capas juntas. Además, debido a las diferentes implementaciones de las transformaciones en varios navegadores, esta también es la solución más compatible en la actualidad.


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

Este enfoque funciona con imágenes grandes (u otros elementos que se pueden escribir fácilmente en un lienzo) y, sin duda, trabajar con grandes bloques de texto sería más difícil, pero, según el sitio, puede ser la solución más adecuada. Si tienes que trabajar con texto en el lienzo, deberías usar el método de la API fillText, pero depende de la accesibilidad (ya que acabas de rasterizar el texto en un mapa de bits) y ahora tendrás que lidiar con el ajuste de línea y muchos otros problemas. Si es posible evitarlo, realmente debería hacerlo, y probablemente sería mejor si usara el enfoque de transformación anterior.

Dado que llevamos esto lo más lejos posible, no hay motivos para suponer que el trabajo de paralaje se debe realizar dentro de un elemento lienzo. Si el navegador lo admite, podemos usar WebGL. La clave aquí es que WebGL tiene la ruta más directa de todas las APIs a la tarjeta gráfica y, por lo tanto, es el candidato más probable para alcanzar los 60 fps, en especial si los efectos del sitio son complejos.

La reacción inmediata puede ser que WebGL sea excesivo o que no sea universal en cuanto a su compatibilidad, pero si usas algo como Three.js, siempre puedes recurrir a un elemento de lienzo y abstraer tu código de manera coherente y amigable. Lo único que debemos hacer es usar Modernizr para verificar la compatibilidad adecuada de la API:

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

Para finalizar, si no te gusta agregar elementos adicionales a la página, puedes usar un lienzo como elemento de fondo en los navegadores Firefox y WebKit. Esto no es siempre visible, así que, como de costumbre, debe tratarse con precaución.

La decisión es tuya

El motivo principal por el que los desarrolladores usan elementos de posicionamiento absoluto de forma predeterminada en lugar de cualquiera de las otras opciones puede ser simplemente la ubicuidad de la compatibilidad. Esto es, hasta cierto punto, ilusorio, ya que es probable que los navegadores más antiguos a los que se dirigen proporcionen una experiencia de procesamiento extremadamente deficiente. ¡Incluso en los navegadores modernos de hoy en día, usar elementos absolutamente posicionados no necesariamente genera un buen rendimiento!

Las transformaciones, que son del tipo 3D, te ofrecen la posibilidad de trabajar directamente con elementos del DOM y lograr una velocidad de fotogramas sólida. La clave del éxito aquí es evitar pintar siempre que puedas y simplemente intentar mover los elementos de un lugar a otro. Recuerda que la manera en que los navegadores WebKit crean capas no se correlaciona necesariamente con otros motores de navegador, así que asegúrate de probarlo antes de decidirte a usar esa solución.

Si tu objetivo solo es el nivel superior de navegadores y puedes renderizar el sitio con lienzos, esa es la mejor opción para ti. Por supuesto, si usaras Three.js, deberías poder intercambiar entre procesadores con mucha facilidad según la compatibilidad que necesites.

Conclusión

Evaluamos algunos enfoques para trabajar con sitios con paralaje, desde elementos absolutamente posicionados hasta el uso de un lienzo de posición fija. La implementación que realices, por supuesto, dependerá de lo que intentes lograr y del diseño específico con el que estés trabajando, pero siempre es bueno saber que tienes opciones.

Como siempre, independientemente del enfoque que intentes usar, no lo adivines, pruébalo.