Complejidades de un desplazador infinito

Resumen: Vuelve a usar los elementos del DOM y quita los que estén lejos del viewport. Usa marcadores de posición para tener en cuenta los datos retrasados. A continuación, se incluye una demostración y el código para el desplazador infinito.

Aparecen desplazadores infinitos en toda Internet. La lista de artistas de Google Music es una, la línea de tiempo de Facebook y el feed en vivo de Twitter también lo es. Te desplazas hacia abajo y, antes de llegar al fondo, el contenido nuevo aparece mágicamente de la nada. Es una experiencia fluida para los usuarios y es fácil ver el atractivo.

Sin embargo, el desafío técnico detrás de una barra de desplazamiento infinito es más difícil de lo que parece. La variedad de problemas que encuentras cuando quieres hacer The Right ThingTM es muy amplia. Comienza con elementos simples, como los vínculos del pie de página que se vuelven prácticamente inalcanzables, ya que el contenido se desplaza a otra parte. Pero los problemas se vuelven más difíciles. ¿Cómo se controla un evento de cambio de tamaño cuando alguien cambia el teléfono del modo vertical al horizontal o cómo se evita que el teléfono se detenga de forma dolorosa cuando la lista se vuelve demasiado larga?

The rightTM

Pensamos que esa era una razón suficiente para crear una implementación de referencia que muestre una manera de abordar todos estos problemas de una manera reutilizable mientras se mantienen los estándares de rendimiento.

Usaremos 3 técnicas para lograr nuestro objetivo: reciclaje de DOM, tombstones y ancla de desplazamiento.

Nuestro caso de demostración será una ventana de chat similar a la de Hangouts en la que podemos desplazarnos por los mensajes. Lo primero que necesitamos es una fuente infinita de mensajes de chat. Técnicamente, ninguno de los desplazadores infinitos es realmente infinito, pero con la cantidad de datos disponibles para insertarse en esos desplazadores, también podrían serlo. Por cuestiones de simplicidad, simplemente codificaremos un conjunto de mensajes de chat y elegiremos el mensaje, el autor y el archivo adjunto de imagen ocasional al azar con un poco de retraso artificial para comportarse un poco más como la red real.

Captura de pantalla de la app de chat

Reciclaje de DOM

El reciclaje del DOM es una técnica poco utilizada para mantener bajo el recuento de nodos del DOM. La idea general es usar elementos del DOM ya creados que estén fuera de la pantalla en lugar de crear elementos nuevos. Es cierto que los nodos del DOM son económicos, pero no son gratuitos, ya que cada uno de ellos agrega un costo adicional en memoria, diseño, estilo y pintura. Los dispositivos de gama baja se volverán notoriamente más lentos si no se pueden usar por completo si el sitio web tiene un DOM demasiado grande para administrar. Además, ten en cuenta que cada rediseño y nueva aplicación de tus estilos (un proceso que se activa cada vez que se agrega o quita una clase de un nodo) aumenta el costo con un DOM más grande. Cuando se reciclan los nodos del DOM, se mantendrá la cantidad total de nodos del DOM considerablemente menor, lo que acelerará todos estos procesos.

El primer obstáculo es el desplazamiento en sí. Dado que solo tendremos un pequeño subconjunto de todos los elementos disponibles en el DOM en cualquier momento, debemos encontrar otra manera de hacer que la barra de desplazamiento del navegador refleje correctamente la cantidad de contenido que, en teoría, está presente. Usaremos un elemento centinela de 1px por 1px con una transformación para forzar que el elemento que contiene los elementos (la pista) tenga la altura deseada. Promocionaremos cada elemento de la pista en su propia capa para asegurarnos de que la capa de la pista esté completamente vacía. Nada de color de fondo. Si la capa de la pista no está vacía, no es apta para las optimizaciones del navegador y tendremos que almacenar una textura en nuestra tarjeta gráfica que tenga una altura de dos cientos de miles de píxeles. Definitivamente no es viable en un dispositivo móvil.

Cada vez que nos desplacemos, verificaremos si el viewport se acercó lo suficiente al final de la pista de aterrizaje. Si es así, extenderemos la pista moviendo el elemento centinela, moviendo los elementos que salieron del viewport a la parte inferior de la pasarela y los propagaremos con contenido nuevo.

Lo mismo ocurre con el desplazamiento en la dirección opuesta. Sin embargo, nunca reduciremos el margen en nuestra implementación para que la posición de la barra de desplazamiento permanezca coherente.

Lápidas

Como mencionamos antes, intentamos que nuestra fuente de datos se comporte como algo en el mundo real. Con latencia de red y todo lo demás. Eso significa que, si nuestros usuarios utilizan el desplazamiento rápido, pueden desplazarse fácilmente más allá del último elemento del que tengamos datos. Si eso sucede, colocaremos un elemento de exclusión (un marcador de posición) que se reemplazará por el elemento con contenido real una vez que se reciban los datos. Las tombstones también se reciclan y tienen un grupo independiente para los elementos del DOM reutilizables. Necesitamos esta información para hacer una buena transición de una tombstone al elemento propagado con contenido, que, de otro modo, resultaría muy molesto para el usuario y podría hacerle perder de vista en qué se estaba enfocando.

Esa tumba. Muy piedra. ¡Vaya!

Un desafío interesante es que los elementos reales pueden tener una altura mayor que los elementos de la lápida debido a la diferente cantidad de texto por elemento o una imagen adjunta. Para solucionar este problema, ajustaremos la posición de desplazamiento actual cada vez que entren datos y se reemplace una tombstone sobre el viewport, anclando la posición de desplazamiento a un elemento en lugar de un valor de píxel. Este concepto se llama ancla de desplazamiento.

Anclaje de desplazamiento

Nuestro anclaje de desplazamiento se invocará tanto cuando se reemplacen las tombstones como cuando se cambie el tamaño de la ventana (lo que también sucede cuando se giran los dispositivos). Tendremos que averiguar cuál es el elemento visible en la parte superior del viewport. Como ese elemento solo podría ser parcialmente visible, también almacenaremos el desplazamiento desde la parte superior del elemento donde comienza el viewport.

Diagrama de anclaje de desplazamiento

Si se cambia el tamaño del viewport y la pista tiene cambios, podemos restablecer una situación que parece visualmente idéntica al usuario. y gana. Excepto cuando se cambia el tamaño de la ventana, cada elemento posiblemente haya cambiado su altura, entonces, ¿cómo sabemos a qué distancia debe colocarse el contenido anclado? Nosotros no. Para averiguarlo, tendríamos que diseñar cada elemento por encima del elemento fijo y sumar todas sus alturas; esto podría causar una pausa significativa después de un cambio de tamaño, y no queremos eso. En cambio, recurrimos a suponer que cada elemento anterior tiene el mismo tamaño que una tombstone y ajustamos la posición de desplazamiento en consecuencia. A medida que los elementos se desplazan por la pista, ajustamos la posición de desplazamiento, lo que permite diferir el trabajo de diseño de manera efectiva cuando sea necesario.

Diseño

Me omití un detalle importante: el diseño. Normalmente, cada reciclaje de un elemento del DOM rediseñaría toda la pista, lo que nos mantendría muy por debajo de nuestro objetivo de 60 fotogramas por segundo. Para evitar esto, nos tomamos la carga del diseño y usamos elementos de posicionamiento absoluto con transformaciones. De esta manera, podemos simular que todos los elementos que están más arriba en la pista siguen ocupando espacio cuando, en realidad, solo hay espacio vacío. Como nosotros hacemos el diseño, podemos almacenar en caché las posiciones donde termina cada elemento y cargar de inmediato el elemento correcto desde la caché cuando el usuario se desplaza hacia atrás.

Idealmente, los elementos solo se volverían a pintar una vez cuando se adjunten al DOM y no se generen interrupciones por las adiciones o eliminaciones de otros elementos en la pista. Eso es posible, pero solo con los navegadores modernos.

Ajustes de vanguardia

Recientemente, Chrome agregó compatibilidad con la contención de CSS, una función que permite a los desarrolladores indicarle al navegador que un elemento es un límite para el trabajo de diseño y pintura. Como estamos haciendo el diseño aquí, es una excelente aplicación de contención. Cada vez que agregamos un elemento a la pista, sabemos que los demás elementos no necesitan verse afectados por el nuevo diseño. Por lo tanto, cada elemento debería obtener contain: layout. Tampoco queremos afectar el resto de nuestro sitio web, por lo que la pasarela en sí también debería obtener esta directiva de estilo.

Otro aspecto que consideramos es el uso de IntersectionObservers como mecanismo para detectar cuándo el usuario se desplaza lo suficiente como para que podamos comenzar a reciclar elementos y cargar datos nuevos. Sin embargo, se especifica que IntersectionObservers tiene una latencia alta (como si se usara requestIdleCallback), por lo que, en realidad, podríamos sentirnos menos responsivos con IntersectionObservers que sin ella. Incluso nuestra implementación actual que usa el evento scroll tiene este problema, ya que los eventos de desplazamiento se envían según el "mejor esfuerzo". En última instancia, el Worklet del compositor de Houdini sería la solución de alta fidelidad para este problema.

Sigue siendo inexacto

Nuestra implementación actual del reciclaje del DOM no es ideal, ya que agrega todos los elementos que pasan por el viewport, en lugar de solo importar los que están en la pantalla. Esto significa que, cuando te desplazas realmente rápido, te pones tanto trabajo de diseño y pintura en Chrome que no funciona. Solo verás el fondo. No es el fin del mundo, sino que hay que mejorar.

Esperamos que veas lo desafiantes que pueden ser los problemas simples cuando deseas combinar una gran experiencia del usuario con altos estándares de rendimiento. Ahora que las apps web progresivas se convertirán en experiencias principales en los teléfonos celulares, esto será más importante y los desarrolladores web deberán seguir invirtiendo en el uso de patrones que respeten las restricciones de rendimiento.

Puedes encontrar todo el código en nuestro repositorio. Hicimos todo lo posible para que sea reutilizable, pero no lo publicaremos como una biblioteca real en npm ni como un repositorio independiente. El uso principal es educativo.