3 способа визуализации больших списков в Angular

Мы рассмотрим следующие техники: виртуальная прокрутка (с использованием Angular CDK), ручной рендеринг и прогрессивный рендеринг. Несмотря на то, что для их реализации мы будем использовать Angular, они также применимы и к другим фреймворкам.

1. Виртуальная прокрутка

Виртуальная прокрутка — наиболее эффективный способ обработки больших списков. Благодаря Angular CDK и другим плагинам ее можно с легкостью реализовать в любом компоненте.

Концепция проста:

  • элемент отображается только в том случае, если он находится в пределах видимых границ контейнера.

Чтобы использовать модуль прокрутки CDK, для начала нужно его установить:

npm i @angular/cdk

Затем импортируем модуль:

import { ScrollingModule } from '@angular/cdk/scrolling';

@NgModule({
 ...
 imports: [ ScrollingModule, ...]
})
export class AppModule {}  

Теперь виртуальную прокрутку можно использовать в компонентах:

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

Несмотря на простоту реализации, результаты впечатляют. Компонент с легкостью отображает тысячи элементов.

Если виртуальная прокрутка настолько хороша и проста в реализации, то зачем использовать другие методы? На это есть несколько причин:

  • Результат сильно зависит от реализации: трудно управлять всеми возможными сценариями с помощью одной единственной реализации. Чем сложнее элемент, тем сложнее предсказать результат.
  • Модуль — это еще один большой кусок кода, добавленный в приложение.
  • Доступность и практичность: скрытые элементы не отображаются и, следовательно, не доступны для поиска.

Виртуальная прокрутка — отличный вариант (если она работает) при наличии:

  • неопределенного и, возможно, огромного списка элементов (предположительно больше 5 Кб, однако зависит от сложности каждого элемента);
  • бесконечной прокрутки элементов.

2. Ручной рендеринг

Один из вариантов ускорения рендеринга большого списка элементов — это ручной рендеринг с использованием API Angular, вместо *ngFor.

Возьмем простой шаблон цикла 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>

Для расчета рендеринга 10000 простых элементов мы будем использовать бенчмарк, вдохновленный js-frameworks-benchmark.

Первый тест выполнен с помощью *ngFor. Результаты: на скриптинг потребовалось 1099 мс, на рендеринг — 1553 мс, а на покрас — 3 мс.

С помощью 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>

Код контроллера изменяется следующим образом:

  • объявляем шаблон и контейнер
@ViewChild('itemsContainer', { read: ViewContainerRef }) container: ViewContainerRef;
@ViewChild('item', { read: TemplateRef }) template: TemplateRef<any>;
  • после создания данных визуализируем их с помощью метода ViewContainerRef createEmbeddedViewwhen:
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
    });
  }
}

Результаты показывают небольшое улучшение:

  • Скриптинг — 734 мс, рендеринг — 1443 мс и покрас — 2 мс:

Тем не менее на практике этот процесс все еще недостаточно быстрый, поскольку при нажатии кнопки браузер зависает на несколько секунд.

Процесс выглядит следующим образом (движения мышь имитируют индикатор загрузки ?):

Теперь перейдем к прогрессивному рендерингу в сочетании с ручным.

3. Прогрессивный рендеринг

Концепция прогрессивного рендеринга заключается в том, чтобы визуализировать поднабор элементов и отложить рендеринг других элементов в цикле событий, что позволяет браузеру плавно и последовательно отображать все элементы.

Код очень простой:

  • мы создаем интервал каждые 10 мс и отображаем 500 элементов одновременно;
  • когда все элементы будут отображены согласно индексу, мы останавливаем интервал и прерываем цикл.
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);

Обратите внимание, что количество отображаемых элементов и время интервала полностью зависят от конкретного случая. Например, при наличии большого количества сложных элементов рендеринг 500 из них будет очень медленным.

Как видите, статистика выглядит хуже:

Однако увеличение времени рендеринга остается незаметным для пользователя, поскольку 500 элементов отображаются одновременно, а сам процесс происходит за пределами контейнера.

При изменении размера контейнера или положения прокрутки в некоторых случаях могут возникнуть проблемы, которые необходимо устранить.

Процесс выглядит следующим образом:

Заключение

Вышеуказанные методы будут полезны, если виртуальная прокрутка оказывается не лучшим вариантом.

Однако во многих случаях виртуальная прокрутка с использованием такой великолепной библиотеки, как Angular CDK, является лучшим способом работы с большими списками.

Читайте также:


Перевод статьи Giancarlo Buomprisco: 3 Ways to Render Large Lists in Angular