[Перевод] 3 способа рендеринга больших списков в Angular

В 2020 году фронтенд-фреймворки стали лучше, эффективнее и быстрее. Но, даже учитывая это, рендеринг больших списков без «замораживания» браузера всё ещё может оказаться сложной задачей даже для самых быстрых из существующих фреймворков.

Это — один из тех случаев, когда «фреймворк является быстрым, а код — медленным».

qlap04o-7mdhzho6nedwbgz64gk.jpeg

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

Автор статьи, перевод которой мы сегодня публикуем, хочет исследовать существующие способы вывода больших списков на веб-страницах и поговорить о сферах их применения.
Хотя этот материал направлен на Angular, то, о чём здесь пойдёт речь, применимо к другим фреймворкам и к проектам, которые написаны на чистом JavaScript. В частности, здесь будут рассмотрены следующие подходы к рендерингу больших списков:

  • Виртуальный скроллинг (с использованием Angular CDK).
  • Ручной рендеринг.
  • Прогрессивный рендеринг.


Виртуальный скроллинг


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

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

Воспользуемся модулем scrolling из Angular CDK, который предназначен для организации виртуального скроллинга. Для этого сначала нужно установить CDK:

npm i @angular/cdk


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

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


После этого в компонентах можно использовать cdk-virtual-scroll-viewport:


 
   {{ item }}  


Вот пример проекта, в котором используется такой подход к организации виртуального скроллинга.

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

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

  • То, как именно будет работать виртуальный скроллинг, сильно зависит от его реализации. Непросто перекрыть все возможные сценарии вывода больших списков, пользуясь единственной реализацией этой идеи. Например, мой компонент зависит от поля Autocomplete (созданного той же командой разработчиков). К сожалению, то, что у меня получилось, не работало так, как ожидалось. Чем сложнее элементы списка — тем больше неожиданностей может возникнуть при их выводе.
  • Дополнительный модуль для организации виртуального скроллинга — это большой фрагмент кода, добавляемый к коду приложения.
  • В сфере доступности и простоты использования содержимого списка с виртуальным скроллингом имеются некоторые проблемы. Скрытые элементы не рендерятся — это значит, что они не будут доступны для средств чтения с экрана, и то, что их нельзя найти на странице, пользуясь стандартными механизмами браузера.


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

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


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


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

У нас имеется простой шаблон, в котором используется цикл, организуемый с помощью директивы *ngFor


  
    {{ item.id }}
  

  
    {{ item.label }}
  

  
    
      
    
  


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

Сначала я исследовал производительность списка, при выводе которого используется обычный цикл *ngFor. В результате оказалось, что выполнение кода (Scripting) заняло 1099 мс., на рендеринг (Rendering) ушло 1553 мс., а на рисование (Painting) — 3 мс.

bdc65d6174798028877e98bdcc7830d8.png


Исследование производительности списка, при выводе которого используется *ngFor

Элементы списка можно рендерить вручную, воспользовавшись API Angular:


  


  
    
      {{ item.id }}
    

    
      {{ item.label }}
    

    
      
        
      
    
  


Поговорим о том, как изменился код контроллера.

Мы объявили шаблон и контейнер:

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


При формировании данных мы рендерим их с использованием метода createEmbeddedView сущности 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
    });
  }
}


В результате показатели, характеризующие производительность списка, удалось немного улучшить. А именно — на выполнение кода ушло 734 мс., на рендеринг — 1443 мс., на рисование — 2 мс.

a673dd0c5ca9c97dc057124d056646e7.png


Исследование производительности списка, при выводе которого используется API Angular

Правда, на практике это означает, что список всё ещё работает крайне медленно. При нажатии на соответствующую кнопку браузер «замерзает» на несколько секунд. Если бы такое появилось в продакшне, пользователям это точно не понравилось бы.

Вот как это выглядит (тут я, с помощью мыши, имитирую индикатор загрузки).

f455a36bc281147be801caed867b1f1f.gif


Медленная работа списка

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

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


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

Код, приведённый ниже, реализующий прогрессивный рендеринг списка, устроен совсем несложно:

  • Сначала мы, пользуясь setInterval, налаживаем регулярный, выполняемый каждые 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);


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

Измерив производительность этого решения, я получил результаты, которые выглядят хуже, чем те, что получал раньше. Выполнение кода — 907 мс., рендеринг — 2555 мс., рисование — 16 мс.

8eb91ebdce039a630cb7ff043afdcb38.png


Исследование производительности списка, при выводе которого используется прогрессивный рендеринг

Но вот пользователь, работая с таким списком, испытает куда более приятные ощущения, чем раньше. Хотя время, необходимое на рендеринг списка, увеличилось, пользователь этого не заметит. Мы, за один заход, рендерим 500 элементов. При этом рендеринг выполняется за пределами границ контейнера.

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

Вот как выглядит работа с таким списком.

d8534a33c099a4f62a561802db2ff995.gif


Список работает быстро

Итоги


Методики ручного и прогрессивного рендеринга больших списков, безусловно, полезны в некоторых ситуациях. Я использовал их в тех случаях, когда виртуальный скроллинг по каким-то причинам мне не подходил.

Учитывая вышесказанное, можно сказать, что чаще всего виртуальный скроллинг, построенный на базе хорошей библиотеки, вроде Angular CDK, это — лучший способ вывода больших списков. Но если виртуальным скроллингом почему-то воспользоваться нельзя — у разработчика есть и другие возможности.

Уважаемые читатели! Как вы выполняете вывод больших списков в своих веб-проектах?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru