Introducción
Los sitios con paralaje se han puesto de moda recientemente. Solo míralo:
- Old Pulteney Row to the Pole
- Adidas Snowboarding
- BBC News - James Bond: Cars, catchphrases and kisses
Si no los conoces, son los sitios en los que la estructura visual de la página cambia a medida que te desplazas. Por lo general, los elementos de la página se escalan, rotan o mueven proporcionalmente a la posición de desplazamiento en la página.
Independientemente de si te gustan o no los sitios con paralaje, lo que puedes decir con bastante seguridad es que son un agujero negro en cuanto al rendimiento. El motivo es que los navegadores suelen estar optimizados para el caso en el que aparece contenido nuevo en la parte superior o inferior de la pantalla cuando te desplazas (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. En el caso de un sitio de paralaje, esto rara vez sucede, ya que, a menudo, cambian los elementos visuales grandes de toda la página, lo que hace que el navegador vuelva a pintar toda la página.
Es razonable generalizar un sitio con paralaje de la siguiente manera:
- Elementos de fondo que, a medida que te desplazas hacia arriba y abajo, cambian de posición, rotación y escala.
- Contenido de la página, como texto o imágenes más pequeñas, que se desplaza de la 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 te recomendamos que lo leas si aún no lo hiciste.
La pregunta es si, cuando creas un sitio con desplazamiento de paralaje, estás limitado a volver a pintar de forma costosa o si hay enfoques alternativos que puedes adoptar para maximizar el rendimiento. Veamos nuestras opciones.
Opción 1: Usa elementos DOM y posiciones absolutas
Este parece ser el enfoque predeterminado que adopta la mayoría de las personas. Hay muchos elementos en la página y, cada vez que se activa un evento de desplazamiento, se realizan muchas actualizaciones visuales para transformarlos.
Si inicias la línea de tiempo de DevTools en el modo de fotogramas y te desplazas, notarás que hay operaciones de pintura de pantalla completa costosas 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.
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 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, pero, como mencionamos en artículos anteriores sobre animaciones más ágiles y eficientes con requestAnimationFrame y rendimiento del desplazamiento, esto no coincide con el programa de actualización del navegador, por lo que se pierden fotogramas o se realiza demasiado trabajo en cada uno. Eso podría generar una sensación de inestabilidad y poco natural en tu sitio, lo que genera usuarios decepcionados y gatitos tristes.
Quitemos 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 ligera mejora, aunque no mucho. El motivo es que la operación de diseño que activamos con el desplazamiento no es tan costosa, pero en otros casos de uso sí lo podría ser. Ahora, al menos, solo realizamos una operación de diseño en cada fotograma.
Ahora podemos controlar uno o cien eventos de desplazamiento por fotograma, pero, lo que es más importante, solo almacenamos el valor más reciente para usarlo cada vez que se ejecuta la devolución de llamada de requestAnimationFrame
y se realizan 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 adecuada para hacerlo. ¿No eres dulce?
El principal problema con este enfoque, ya sea con requestAnimationFrame
o sin él, es que, en esencia, tenemos una capa para toda la página y, cuando movemos estos elementos visuales, necesitamos volver a pintar grandes áreas (y costosas). Por lo general, el pintado es una operación de bloqueo (aunque eso está cambiando), lo que significa que el navegador no puede realizar ninguna otra tarea y, a menudo, superamos el presupuesto de nuestro fotograma de 16 ms, y todo sigue siendo inestable.
Opción 2: Usa elementos 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 los elementos con las transformaciones 3D aplicadas reciben una nueva capa por elemento y, en los navegadores WebKit, a menudo también se produce un cambio al compositor de hardware. En la opción 1, en cambio, teníamos una capa grande para la página que se debía volver a pintar cuando cambiaba algo, y la CPU se encargaba de toda la pintura y composición.
Eso significa que, con esta opción, las cosas son diferentes: potencialmente, tenemos una capa para cualquier elemento al que apliquemos una transformación 3D. Si, a partir de este punto, solo realizamos más transformaciones en los elementos, no necesitaremos volver a pintar la capa, y la GPU puede mover los elementos y combinar la página final.
Muchas veces, las personas simplemente usan el hack de -webkit-transform: translateZ(0);
y ven mejoras mágicas en el rendimiento. Si bien esto funciona en la actualidad, existen problemas:
- No es compatible con todos los navegadores.
- Forzar la mano del navegador creando una capa nueva para cada elemento transformado. Muchas capas pueden generar otros cuellos de botella de rendimiento, así que úsalas con moderación.
- Se inhabilitó para algunos puertos de WebKit (cuarto punto de la parte inferior).
Si decides usar la ruta de traducción 3D, ten cuidado, ya que es una solución temporal al problema. Idealmente, veríamos características de renderización similares de las transformaciones 2D como lo hacemos con las 3D. Los navegadores avanzan a un ritmo fenomenal, así que espero que antes de eso veamos eso.
Por último, debes evitar las pinturas siempre que sea posible y simplemente mover los elementos existentes por la página. A modo de ejemplo, un enfoque típico en los sitios de paralaje es usar divs de altura fija y cambiar su posición de fondo para proporcionar el efecto. Lamentablemente, eso significa que el elemento se debe volver a pintar en cada pasada, lo que puede afectar el rendimiento. En su lugar, si puedes, crea el elemento (únelo dentro de un div con overflow: hidden
si es necesario) y simplemente tradúcelo.
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 el que dibujaremos nuestras imágenes transformadas. A primera vista, podría no parecer la solución con el mejor rendimiento, pero este enfoque tiene algunos beneficios:
- Ya no necesitamos tanto trabajo del compositor porque solo tenemos un elemento, el lienzo.
- En realidad, estamos tratando con un solo mapa de bits con aceleración de hardware.
- La API de Canvas2D es ideal para el tipo de transformaciones que queremos realizar, lo que significa que el desarrollo y el mantenimiento son más fáciles de administrar.
El uso de un elemento de lienzo nos brinda una capa nueva, pero es solo una capa, mientras que en la opción 2, en realidad, se nos proporcionó una capa nueva para cada elemento con una transformación 3D aplicada, por lo que tenemos una mayor carga de trabajo para combinar todas esas capas. Esta también es la solución más compatible en la actualidad, en función de las diferentes implementaciones de transformaciones en varios navegadores.
/**
* 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 muy bien cuando se trata de imágenes grandes (o de otros elementos que se pueden escribir fácilmente en un lienzo) y, por supuesto, trabajar con grandes bloques de texto sería más desafiante, pero, según tu sitio, podría ser la solución más apropiada. Si tienes que lidiar con texto en el lienzo, deberás usar el método de la API de fillText
, pero esto implicará una pérdida de accesibilidad (acabas de rasterizar el texto en un mapa de bits) y deberás lidiar con el ajuste de líneas y muchos otros problemas. Si puedes evitarlo, te recomendamos que lo hagas. Es probable que te resulte más útil usar el enfoque de transformaciones anterior.
Dado que estamos llevando esto lo más lejos posible, no hay razón para suponer que el trabajo de paralaje debe realizarse dentro de un elemento de lienzo. Si el navegador lo admite, podríamos 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 la opción más probable para lograr 60 fps, especialmente si los efectos del sitio son complejos.
Tu reacción inmediata podría ser que WebGL es excesivo o que no es omnipresente en términos de compatibilidad, pero si usas algo como Three.js, siempre puedes recurrir al uso de un elemento de lienzo y tu código se abstrae de una manera coherente y amigable. Lo único que tenemos que hacer es usar Modernizr para verificar la compatibilidad con la API adecuada:
// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
renderer = new THREE.CanvasRenderer();
}
Como último comentario sobre este enfoque, si no te gusta agregar elementos adicionales a la página, siempre puedes usar un lienzo como elemento de fondo en Firefox y en navegadores basados en WebKit. Por supuesto, esto no es algo omnipresente, por lo que, como de costumbre, debes tener cuidado.
La decisión es tuya
El motivo principal por el que los desarrolladores usan de forma predeterminada elementos con posición absoluta en lugar de cualquiera de las otras opciones puede ser simplemente la ubiquidad de la compatibilidad. Esto es, en cierto modo, ilusorio, ya que es probable que los navegadores más antiguos a los que se segmenta proporcionen una experiencia de renderización extremadamente deficiente. Incluso en los navegadores modernos de hoy en día, usar elementos con posicionamiento absoluto no siempre genera un buen rendimiento.
Las transformaciones, en especial las 3D, te permiten trabajar directamente con elementos DOM y lograr una velocidad de fotogramas sólida. La clave del éxito aquí es evitar pintar donde sea posible y simplemente intentar mover los elementos. Ten en cuenta que la forma en que los navegadores WebKit crean capas no se correlaciona necesariamente con otros motores de navegador, así que asegúrate de probarlo antes de comprometerte con esa solución.
Si tu objetivo es solo el nivel superior de navegadores y puedes renderizar el sitio con lienzos, esa podría ser la mejor opción para ti. Si usaras Three.js, deberías poder intercambiar y cambiar entre renderizadores con mucha facilidad según la compatibilidad que necesites.
Conclusión
Analizamos algunos enfoques para abordar los sitios de paralaje, desde elementos con posicionamiento absoluto hasta el uso de un lienzo de posición fija. Por supuesto, la implementación que realices 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.
Y, como siempre, independientemente del enfoque que pruebes, no lo adivines, pruébalo.