Virtualiza listas grandes con una ventana de reacción

Las tablas y listas muy grandes pueden ralentizar considerablemente el rendimiento de tu sitio. La virtualización puede ayudar.

react-window es una biblioteca que permite renderizar de manera eficiente listas grandes.

El siguiente es un ejemplo de una lista que contiene 1,000 filas que se renderizan con react-window. Intenta desplazarte lo más rápido posible.

¿Por qué es útil?

Puede haber ocasiones en las que necesites mostrar una tabla grande o una lista que contenga muchas filas. Cargar todos los elementos de esa lista puede afectar el rendimiento de manera significativa.

La virtualización de listas, o “sistema de ventanas”, es el concepto de renderizar solo lo que es visible para el usuario. La cantidad de elementos que se renderizan al principio es un subconjunto muy pequeño de toda la lista, y la "ventana" de contenido visible se mueve cuando el usuario continúa desplazándose. Esto mejora el rendimiento de renderización y de desplazamiento de la lista.

Ventana de contenido en una lista virtualizada
Cómo mover una “ventana” de contenido a una lista virtualizada

Los nodos del DOM que salen de la "ventana" se reciclan o se reemplazan de inmediato por elementos más nuevos a medida que el usuario se desplaza hacia abajo en la lista. Esto mantiene la cantidad de todos los elementos renderizados específicos del tamaño de la ventana.

ventana de reacción

react-window es una pequeña biblioteca de terceros que facilita la creación de listas virtualizadas en tu aplicación. Proporciona varias APIs base que se pueden usar para diferentes tipos de listas y tablas.

Cuándo utilizar listas de tamaño fijo

Usa el componente FixedSizeList si tienes una lista larga y unidimensional de elementos del mismo tamaño.

import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const ListComponent = () => (
  <FixedSizeList
    height={500}
    width={500}
    itemSize={120}
    itemCount={items.length}
  >
    {Row}
  </FixedSizeList>
);

export default ListComponent;
  • El componente FixedSizeList acepta un prop height, width y itemSize para controlar el tamaño de los elementos dentro de la lista.
  • Una función que renderiza las filas se pasa como un elemento secundario a FixedSizeList. Se puede acceder a los detalles sobre un elemento específico con el argumento index (items[index]).
  • También se pasa un parámetro style al método de renderización de filas que debe adjuntarse al elemento de la fila. Los elementos de lista están absolutamente posicionados con sus valores de altura y ancho asignados intercalados, y el parámetro style es responsable de esto.

En el ejemplo de Glitch que se mostró antes en este artículo, se muestra un ejemplo de un componente FixedSizeList.

Cuándo usar listas de tamaño variable

Usa el componente VariableSizeList para renderizar una lista de elementos que tengan diferentes tamaños. Este componente funciona de la misma manera que una lista de tamaño fijo, pero espera una función para la propiedad itemSize en lugar de un valor específico.

import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const getItemSize = index => {
  // return a size for items[index]
}

const ListComponent = () => (
  <VariableSizeList
    height={500}
    width={500}
    itemCount={items.length}
    itemSize={getItemSize}
  >
    {Row}
  </VariableSizeList>
);

export default ListComponent;

En la siguiente incorporación, se muestra un ejemplo de este componente.

La función de tamaño del elemento que se pasa al prop itemSize aleatoriza las alturas de las filas de este ejemplo. Sin embargo, en una aplicación real, debería haber una lógica real que defina los tamaños de cada elemento. Lo ideal es que estos tamaños se calculen a partir de datos o que se obtengan de una API.

Cuadrículas

react-window también proporciona compatibilidad para virtualizar listas o cuadrículas multidimensionales. En este contexto, la "ventana" de contenido visible cambia a medida que el usuario se desplaza horizontalmente y verticalmente.

La ventana de contenido en movimiento en una cuadrícula virtualizada es bidimensional
La “ventana” de contenido en movimiento de una cuadrícula virtualizada es bidimensional

De manera similar, los componentes FixedSizeGrid y VariableSizeGrid se pueden usar en función de si el tamaño de los elementos específicos de la lista puede variar.

  • Para FixedSizeGrid, la API es casi igual, pero teniendo en cuenta que las alturas, los anchos y los recuentos de elementos deben representarse en columnas y filas.
  • Para VariableSizeGrid, se pueden cambiar los anchos de las columnas y las alturas de las filas si pasas funciones en lugar de valores a sus propiedades respectivas.

Consulta la documentación para ver ejemplos de cuadrículas virtualizadas.

Carga diferida durante el desplazamiento

Muchos sitios web mejoran el rendimiento a la espera de cargar y renderizar elementos en una lista larga hasta que el usuario se desplazó hacia abajo. Esta técnica, comúnmente conocida como "carga infinita", agrega nuevos nodos del DOM a la lista a medida que el usuario se desplaza más allá de un umbral determinado que está cerca del final. Aunque esto es mejor que cargar todos los elementos de una lista a la vez, igualmente termina completando el DOM con miles de entradas de fila si el usuario se desplaza más allá de esa cantidad. Esto puede provocar un tamaño del DOM demasiado grande, que comienza a afectar el rendimiento a través de los cálculos de estilo y las mutaciones del DOM más lentos.

El siguiente diagrama puede ayudarte a resumir esto:

Diferencia en el desplazamiento entre una lista normal y una virtualizada
Diferencia en el desplazamiento entre una lista normal y una virtualizada

El mejor enfoque para resolver este problema es seguir usando una biblioteca como react-window para mantener una "ventana" pequeña de elementos en una página, pero también para cargar de forma diferida las entradas más nuevas a medida que el usuario se desplaza hacia abajo. Un paquete independiente, react-window-infinite-loader, hace que esto sea posible con react-window.

Considera el siguiente fragmento de código, que muestra un ejemplo de estado que se administra en un componente App superior.

import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [], // instantiate initial list here
      moreItemsLoading: false,
      hasNextPage: true
    };

    this.loadMore = this.loadMore.bind(this);
  }

  loadMore() {
   // method to fetch newer entries for the list
  }

  render() {
    const { items, moreItemsLoading, hasNextPage } = this.state;

    return (
      <ListComponent
        items={items}
        moreItemsLoading={moreItemsLoading}
        loadMore={this.loadMore}
        hasNextPage={hasNextPage}
      />
    );
  }
}

export default App;

Se pasa un método loadMore a un ListComponent secundario que contiene la lista de cargadores infinitos. Esto es importante porque el cargador infinito necesita activar una devolución de llamada para cargar más elementos una vez que el usuario se desplazó más allá de un punto determinado.

A continuación, se muestra cómo se ve el elemento ListComponent que procesa la lista:

import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";

const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
  const Row = ({ index, style }) => (
     {/* define the row component using items[index] */}
  );

  const itemCount = hasNextPage ? items.length + 1 : items.length;

  return (
    <InfiniteLoader
      isItemLoaded={index => index < items.length}
      itemCount={itemCount}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={500}
          width={500}
          itemCount={itemCount}
          itemSize={120}
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {Row}
        </FixedSizeList>
      )}
  </InfiniteLoader>
  )
};

export default ListComponent;

Aquí, el componente FixedSizeList se une a InfiniteLoader. Los props asignados al cargador son los siguientes:

  • isItemLoaded: Es un método que verifica si se cargó un elemento determinado.
  • itemCount: Cantidad de elementos en la lista (o esperados)
  • loadMoreItems: Es la devolución de llamada que muestra una promesa que se resuelve en datos adicionales de la lista.

Una prop de renderización se usa para mostrar una función que el componente de la lista usa en su procesamiento. Los atributos onItemsRendered y ref son atributos que se deben pasar.

El siguiente es un ejemplo de cómo puede funcionar la carga infinita con una lista virtualizada.

Desplazarse hacia abajo en la lista puede parecer lo mismo, pero ahora se realiza una solicitud para recuperar 10 usuarios de una API de usuario aleatorio cada vez que te desplazas cerca del final de la lista. Todo esto se realiza mientras se renderiza solo una "ventana" de resultados a la vez.

Si verificas el index de un elemento determinado, se puede mostrar un estado de carga diferente para un elemento en función de si se realizó una solicitud de entradas más recientes y este aún se está cargando.

Por ejemplo:

const Row = ({ index, style }) => {
  const itemLoading = index === items.length;

  if (itemLoading) {
      // return loading state
  } else {
      // return item
  }
};

Sobrebarrido

Dado que los elementos de una lista virtualizada solo cambian cuando el usuario se desplaza, el espacio en blanco puede parpadear brevemente cuando se muestren entradas más nuevas. Puedes intentar desplazarte rápidamente por cualquiera de los ejemplos anteriores de esta guía para notar esto.

Para mejorar la experiencia del usuario de las listas virtualizadas, react-window te permite sobrebarear elementos con la propiedad overscanCount. Esto te permite definir cuántos elementos fuera de la "ventana" visible se deben renderizar en todo momento.

<FixedSizeList
  //...
  overscanCount={4}
>
  {...}
</FixedSizeList>

overscanCount funciona para los componentes FixedSizeList y VariableSizeList, y tiene un valor predeterminado de 1. Según el tamaño de la lista y el tamaño de cada elemento, sobrepasar más de una entrada puede ayudar a evitar un destello notable de espacio vacío cuando el usuario se desplaza. Sin embargo, sobreanalizar demasiadas entradas puede afectar el rendimiento de forma negativa. El objetivo de usar una lista virtualizada es minimizar el número de entradas a lo que el usuario puede ver en un momento determinado, así que trata de mantener la cantidad de elementos sobrecantados lo más bajo posible.

En FixedSizeGrid y VariableSizeGrid, usa las propiedades overscanColumnsCount y overscanRowsCount para controlar la cantidad de columnas y filas que se sobreanalizarán, respectivamente.

Conclusión

Si no estás seguro de por dónde comenzar a virtualizar listas y tablas en tu aplicación, sigue estos pasos:

  1. Mide el rendimiento de la renderización y el desplazamiento. En este artículo, se muestra cómo se puede usar el medidor de FPS en las Herramientas para desarrolladores de Chrome para explorar la eficiencia con la que se renderizan los elementos en una lista.
  2. Incluye react-window para cualquier lista o cuadrícula larga que afecte el rendimiento.
  3. Si hay ciertas funciones que no son compatibles con react-window, considera usar react-virtualized si no puedes agregar esta funcionalidad por tu cuenta.
  4. Une tu lista virtualizada con react-window-infinite-loader si necesitas cargar elementos de forma diferida a medida que el usuario se desplaza.
  5. Usa la propiedad overscanCount para tus listas y las propiedades overscanColumnsCount y overscanRowsCount para tus cuadrículas para evitar que se muestre contenido vacío. No sobrecargues demasiadas entradas, ya que esto afectará el rendimiento de forma negativa.