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. Aquí tienes una demo y el código del control deslizante infinito.

Los controles de desplazamiento infinito aparecen por todas partes en Internet. La lista de artistas de Google Music es una, el feed de Facebook es uno y el feed en vivo de Twitter también es uno. Te desplazas hacia abajo y, antes de llegar al final, aparece contenido nuevo como por arte de magia. Es una experiencia fluida para los usuarios y es fácil ver su atractivo.

Sin embargo, el desafío técnico detrás de un control deslizante infinito es más difícil de lo que parece. La variedad de problemas que encuentras cuando quieres hacer lo correcto es amplia. Comienza con elementos simples, como que los vínculos del pie de página se vuelven prácticamente inaccesibles porque el contenido sigue alejando el pie de página. Pero los problemas se vuelven más difíciles. ¿Cómo controlas un evento de cambio de tamaño cuando alguien cambia su teléfono del modo vertical al horizontal o cómo evitas que el teléfono se detenga cuando la lista se hace demasiado larga?

The right thing™

Pensamos que eso era motivo suficiente para crear una implementación de referencia que muestre una forma de abordar todos estos problemas de manera reutilizable y, al mismo tiempo, mantener los estándares de rendimiento.

Usaremos 3 técnicas para lograr nuestro objetivo: reciclaje de DOM, lápidas y anclaje de desplazamiento.

Nuestro caso de demostración será una ventana de chat similar a Hangouts, en la que podremos desplazarnos por los mensajes. Lo primero que necesitamos es una fuente infinita de mensajes de chat. Técnicamente, ninguno de los controles de desplazamiento infinitos que existen es verdaderamente ilimitado, pero con la cantidad de datos que están disponibles para ingresar en estos controles, es posible que lo sean. Para simplificar, solo codificaremos un conjunto de mensajes de chat y elegiremos el mensaje, el autor y el archivo adjunto de imagen ocasional de forma aleatoria con un poco de retraso artificial para que se comporte un poco más como la red real.

Captura de pantalla de la app de chat

Reciclaje del DOM

El reciclaje de DOM es una técnica poco utilizada para mantener bajo el recuento de nodos DOM. La idea general es usar elementos DOM ya creados que están fuera de la pantalla en lugar de crear otros nuevos. Es cierto que los nodos 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 ralentizarán notablemente, o incluso serán inutilizables, 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) se vuelve más costoso con un DOM más grande. Reciclar los nodos DOM significa que mantendremos la cantidad total de nodos DOM mucho más baja, lo que hará que todos estos procesos sean más rápidos.

El primer obstáculo es el desplazamiento en sí. Dado que solo tendremos un subconjunto pequeño de todos los elementos disponibles en el DOM en un momento determinado, debemos encontrar otra forma de hacer que la barra de desplazamiento del navegador refleje correctamente la cantidad de contenido que, en teoría, está allí. Usaremos un elemento centinela de 1 px por 1 px 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 a su propia capa para asegurarnos de que la capa de la pista esté completamente vacía. Sin color de fondo, nada. 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 unos doscientos mil píxeles. Definitivamente, no es viable en un dispositivo móvil.

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

Runway Sentinel Viewport

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

Tombstones

Como mencionamos anteriormente, intentamos que nuestra fuente de datos se comporte como algo del mundo real. Con la latencia de red y todo. Esto significa que, si nuestros usuarios usan el desplazamiento rápido, pueden pasar fácilmente por el último elemento del que tenemos datos. Si eso sucede, colocaremos un elemento de lápida, un marcador de posición, que se reemplazará por el elemento con el contenido real una vez que lleguen los datos. Las lápidas también se reciclan y tienen un grupo independiente para los elementos DOM reutilizables. Necesitamos eso para poder hacer una transición agradable de una lápida al elemento propagado con contenido, lo que de otro modo sería muy molesto para el usuario y podría hacer que pierda el rastro de lo que estaba enfocando.

Tal tumba. Muy rocoso. ¡Vaya!

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

Anclaje de desplazamiento

Se invocará nuestro anclaje de desplazamiento cuando se reemplacen las lápidas y cuando se cambie el tamaño de la ventana (lo que también sucede cuando se voltean los dispositivos). Tendremos que averiguar cuál es el elemento visible más alto en el viewport. Como ese elemento solo podría ser visible de forma parcial, 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 de la vista y la pista tiene cambios, podemos restablecer una situación que se sienta visualmente idéntica para el usuario. ¡Gana! Excepto que una ventana con el tamaño modificado significa que cada elemento podría haber cambiado su altura, ¿cómo sabemos qué tan abajo se debe colocar el contenido anclado? No lo hacemos. Para averiguarlo, deberíamos diseñar cada elemento sobre el elemento anclado y sumar todas sus alturas. Esto podría causar una pausa significativa después de cambiar el tamaño, y no queremos eso. En su lugar, suponemos que todos los elementos anteriores tienen el mismo tamaño que una lápida y ajustamos nuestra posición de desplazamiento según corresponda. A medida que los elementos se desplazan hacia la pista, ajustamos nuestra posición de desplazamiento, lo que aplaza de manera eficaz el trabajo de diseño hasta que realmente sea necesario.

Diseño

Omití un detalle importante: el diseño. Por lo general, cada reciclaje de un elemento DOM volvería a diseñar toda la pista, lo que nos llevaría muy por debajo de nuestro objetivo de 60 fotogramas por segundo. Para evitar esto, nos encargamos del diseño y usamos elementos con posicionamiento absoluto con transformaciones. De esta manera, podemos fingir que todos los elementos más arriba de la pista aún ocupan espacio cuando, en realidad, solo hay espacio vacío. Dado que nosotros mismos hacemos el diseño, podemos almacenar en caché las posiciones en las que termina cada elemento y cargar inmediatamente el elemento correcto desde la caché cuando el usuario se desplaza hacia atrás.

Lo ideal es que los elementos solo se vuelvan a pintar una vez cuando se adjuntan al DOM y no se vean afectados por la adición o eliminación de otros elementos en la pista. Eso es posible, pero solo con navegadores modernos.

Ajustes de vanguardia

Recientemente, Chrome agregó compatibilidad con CSS Containment, una función que nos permite a los desarrolladores indicarle al navegador que un elemento es un límite para el diseño y el trabajo de pintura. Dado que nosotros mismos hacemos el diseño aquí, es una aplicación óptima para el confinamiento. Cada vez que agregamos un elemento a la pista, sabemos que los demás elementos no deben verse afectados por el rediseño. Por lo tanto, cada elemento debería obtener contain: layout. Tampoco queremos afectar al resto de nuestro sitio web, así que la propia pista también debe obtener esta directiva de diseño.

Otra cosa que consideramos es usar IntersectionObservers como un mecanismo para detectar cuándo el usuario desfiló 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 es posible que sintamos menos respuesta con IntersectionObservers que sin ellos. Incluso nuestra implementación actual con el evento scroll sufre de este problema, ya que los eventos de desplazamiento se envían de forma "del mejor esfuerzo". En última instancia, la worklet de Compositor de Houdini sería la solución de alta fidelidad para este problema.

Aún no es perfecto

Nuestra implementación actual del reciclaje de DOM no es ideal, ya que agrega todos los elementos que pasan por el viewport, en lugar de solo preocuparse por los que realmente están en la pantalla. Esto significa que, cuando te desplazas muy rápido, le asignas tanto trabajo al diseño y a la pintura en Chrome que no puede seguir el ritmo. No verás nada más que el fondo. No es el fin del mundo, pero es algo que debes mejorar.

Esperamos que veas lo desafiantes que pueden ser los problemas simples cuando quieres combinar una excelente experiencia del usuario con estándares de alto rendimiento. A medida que las apps web progresivas se conviertan en experiencias principales en los teléfonos celulares, esto se volverá 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 se pueda volver a usar, pero no la publicaremos como una biblioteca real en npm ni como un repositorio independiente. El uso principal es educativo.