3 formas de renderizar listas grandes en Angular

En 2020, los marcos front-end se volvieron mejores, más eficientes y más rápidos. Pero incluso con esto en mente, renderizar listas grandes sin congelar el navegador puede ser una tarea desalentadora incluso para los marcos existentes más rápidos.

Este es uno de esos casos donde "el marco es rápido y el código es lento". Existen muchos enfoques que le permiten mostrar una gran cantidad de elementos sin bloquear la interacción del usuario con la página web. El autor del artículo, cuya traducción publicamos hoy, quiere explorar los métodos existentes para mostrar grandes listas en páginas web y hablar sobre sus áreas de aplicación.







Aunque este material está dirigido a Angular, lo que se discute aquí se aplica a otros marcos y proyectos que están escritos en JavaScript puro. En particular, los siguientes enfoques para representar listas grandes se considerarán aquí:

  • Desplazamiento virtual (usando Angular CDK).
  • Representación manual.
  • Render progresivo

Desplazamiento virtual


El desplazamiento virtual es quizás la forma más eficiente de trabajar con listas grandes. Es cierto que tiene algunas desventajas. El desplazamiento virtual, gracias a Angular CDK y otros complementos, es muy fácil de implementar en cualquier componente.

La idea detrás del desplazamiento virtual es simple, pero no siempre es fácil de implementar. La conclusión aquí es que tenemos un contenedor y una lista de elementos. Un elemento se representa solo si está dentro del área visible del contenedor.

Utilizaremos el módulo scrollingde Angular CDK, que está diseñado para organizar el desplazamiento virtual. Para hacer esto, primero debe instalar el CDK:

npm i @angular/cdk

Entonces necesitas importar el módulo:

import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
 ...
 imports: [ ScrollingModule, ...]
})
export class AppModule {}

Después de eso, en los componentes que puede usar cdk-virtual-scroll-viewport:

<cdk-virtual-scroll-viewport itemSize="50">
 <div *cdkVirtualFor="let item of items">
   {{ item }}
 </div>
</cdk-virtual-scroll-viewport>

Aquí hay un proyecto de ejemplo que utiliza este enfoque para organizar el desplazamiento virtual.

Como puede ver, los mecanismos angulares estándar permiten, sin mucha dificultad, lograr resultados impresionantes. El componente en el ejemplo representa muchos miles de elementos sin ningún problema.

Si el desplazamiento virtual es tan bueno y es tan fácil de implementar, surge la pregunta de por qué estudiar otras formas de generar listas grandes. También me interesó esta pregunta. Al final resultó que, este estado de cosas tiene varias razones:

  • , , . , . , Autocomplete ( ). , , , , . — .
  • — , .
  • Hay algunos problemas con la accesibilidad y la facilidad de uso de los contenidos de una lista con desplazamiento virtual. Los elementos ocultos no se representan: esto significa que no estarán disponibles para los lectores de pantalla y que no se pueden encontrar en la página utilizando mecanismos de navegador estándar.

El desplazamiento virtual es ideal en una serie de situaciones (siempre que funcione):

  • En ese caso, si desea mostrar listas cuyo tamaño no se conoce de antemano, o aquellas que pueden ser enormes (según una estimación aproximada, listas que incluyen más de 5 mil elementos, pero esto depende en gran medida de la complejidad de cada elemento).
  • En el caso de que necesite organizar un desplazamiento sin fin.

Renderizado manual


Una de las formas de trabajar con listas que traté de acelerar la salida de un gran conjunto de elementos es utilizar el renderizado manual utilizando la API angular *ngFor.

Tenemos una plantilla simple que usa un ciclo organizado usando la directiva *ngFor

<tr 
*ngFor="let item of data; trackBy: trackById; let isEven = even; let isOdd = odd"
    class="h-12"
    [class.bg-gray-400]="isEven"
    [class.bg-gray-500]="isOdd"
>
  <td>
    <span class="py-2 px-4">{{ item.id }}</span>
  </td>

  <td>
    <span>{{ item.label }}</span>
  </td>

  <td>
    <a>
      <button class="py-2 px-4 rounded (click)="remove(item)">x</button>
    </a>
  </td>
</tr>

Para medir el rendimiento de representación de 10.000 elementos simples, utilicé un punto de referencia basado en js-frameworks-benchmark .

Primero, examiné el rendimiento de una lista, que usa un bucle regular para la salida *ngFor. Como resultado, resultó que la ejecución del código (Scripting) tomó 1099 ms., La representación (Rendering) tomó 1553 ms., Y el dibujo (Pintura) - 3 ms.


Un estudio del rendimiento de una lista, que utiliza * ngFor para la salida, los

elementos de la lista se pueden representar manualmente utilizando la API angular:

<tbody>
  <ng-container #itemsContainer></ng-container>
</tbody>
<ng-template #item let-item="item" let-isEven="isEven">
  <tr class="h-12 "
      [class.bg-gray-400]="isEven"
      [class.bg-gray-500]="!isEven"
  >
    <td>
      <span class="py-2 px-4">{{ item.id }}</span>
    </td>

    <td>
      <span>{{ item.label }}</span>
    </td>

    <td>
      <a>
        <button class="py-2 px-4 rounded" (click)="remove(item)">x</button>
      </a>
    </td>
  </tr>
</ng-template>

Hablemos de cómo ha cambiado el código del controlador.

Declaramos una plantilla y un contenedor:

@ViewChild('itemsContainer', { read: ViewContainerRef }) container: ViewContainerRef;
@ViewChild('item', { read: TemplateRef }) template: TemplateRef<any>;

Al generar datos, los representamos utilizando el método de createEmbeddedViewentidad ViewContainerRef:

private buildData(length: number) {
  const start = this.data.length;
  const end = start + length;

  for (let n = start; n <= end; n++) {
    this.container.createEmbeddedView(this.template, {
      item: {
        id: n,
        label: Math.random()
      },
      isEven: n % 2 === 0
    });
  }
}

Como resultado, los indicadores que caracterizan el rendimiento de la lista se mejoraron ligeramente. Es decir, se necesitaron 734 ms para ejecutar el código, 1443 ms para renderizar, 2 ms para dibujar.


Investigar el rendimiento de una lista que usa la API angular para generar

True, en la práctica, esto significa que la lista sigue siendo extremadamente lenta. Cuando hace clic en el botón correspondiente, el navegador se "congela" durante unos segundos. Si esto apareciera en producción, a los usuarios definitivamente no les gustaría.

Así es como se ve (aquí, usando el mouse, imito el indicador de carga).


Reducción de la lista

ahora, para mejorar la representación manual de la lista, intentaremos aplicar la tecnología de representación progresiva.

Renderizado progresivo


La idea de renderizado progresivo es simple. Consiste en representar un subconjunto de elementos progresivamente, posponiendo la representación de otros elementos en el bucle de eventos. Esto permite que el navegador muestre todos los elementos sin "disminuir la velocidad".

El siguiente código que implementa la representación progresiva de listas es muy sencillo:

  • Primero, usando esto setInterval, establecemos una llamada regular, cada 10 ms. A la función que representa 500 elementos cuando se llama.
  • Después de deducir todos los elementos, que determinamos en función del análisis del índice del elemento actual, detenemos la llamada a la función regular e interrumpimos el ciclo.

private buildData(length: number) {
  const ITEMS_RENDERED_AT_ONCE = 500;
  const INTERVAL_IN_MS = 10;

  let currentIndex = 0;

  const interval = setInterval(() => {
    const nextIndex = currentIndex + ITEMS_RENDERED_AT_ONCE;

    for (let n = currentIndex; n <= nextIndex ; n++) {
      if (n >= length) {
        clearInterval(interval);
        break;
      }
      const context = {
        item: {
          id: n,
          label: Math.random()
        },
        isEven: n % 2 === 0
      };
      this.container.createEmbeddedView(this.template, context);
    }

    currentIndex += ITEMS_RENDERED_AT_ONCE;
  }, INTERVAL_IN_MS);

Tenga en cuenta que el número de elementos que se muestran por llamada a la función que se transfiere setInterval, así como la frecuencia de la llamada a esta función, depende completamente de las circunstancias. Por ejemplo, si los elementos de la lista que se muestran son muy complejos, la salida de 500 de estos elementos de una sola vez será inaceptablemente lenta.

Al medir el rendimiento de esta solución, obtuve resultados que se ven peor que los que recibí anteriormente. Ejecución de código - 907 ms., Representación - 2555 ms., Dibujo - 16 ms.


Investigando el rendimiento de una lista, cuyo resultado utiliza renderizado progresivo,

pero el usuario, trabajando con dicha lista, experimentará sensaciones mucho más agradables que antes. Aunque el tiempo requerido para representar la lista ha aumentado, el usuario no lo notará. Nosotros, de una vez, renderizamos 500 elementos. En este caso, la representación se realiza fuera de los límites del contenedor.

Aquí, pueden surgir algunos problemas debido al hecho de que el contenedor cambia de tamaño durante el renderizado, o la posición del desplazamiento de contenido cambia. Si esto sucede, tendrá que lidiar con problemas similares.

Así es como se ve trabajar con dicha lista.


La lista es rápida

Resumen


Las técnicas de renderizado manual y progresivo para listas grandes son ciertamente útiles en algunas situaciones. Los usé en casos en los que el desplazamiento virtual por alguna razón no me convenía.

Dado lo anterior, podemos decir que la mayoría de las veces, el desplazamiento virtual, construido sobre la base de una buena biblioteca, como Angular CDK, es la mejor manera de mostrar listas grandes. Pero si el desplazamiento virtual no se puede utilizar por alguna razón, el desarrollador tiene otras posibilidades.

¡Queridos lectores! ¿Cómo genera grandes listas en sus proyectos web?


All Articles