Как реализовать виртуальный скроллинг?angular-75

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

Основные принципы виртуального скроллинга

  1. Рендеринг только видимых элементов - отображаются только элементы, попадающие в viewport
  2. Динамическая подгрузка - элементы загружаются/выгружаются по мере скролла
  3. Фиксированный размер элементов - необходимо знать или задавать высоту элементов
  4. Виртуальное пространство - создается "фантомный" контейнер полной высоты списка

Реализация с помощью CDK Virtual Scroll

Angular CDK предоставляет готовое решение для виртуального скроллинга.

1. Установка необходимых модулей

npm install @angular/cdk

2. Импорт модуля виртуального скроллинга

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

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

3. Базовый пример использования

<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
  <div *cdkVirtualFor="let item of items" class="item">
    {{item}}
  </div>
</cdk-virtual-scroll-viewport>
.viewport {
  height: 500px;
  width: 100%;
  border: 1px solid #ccc;
}

.item {
  height: 50px;
  display: flex;
  align-items: center;
  padding: 0 16px;
  box-sizing: border-box;
}

4. Настройка параметров

Основные параметры:

  • itemSize: высота элемента в пикселях (обязательный параметр)
  • minBufferPx: минимальный буфер загрузки (сколько элементов загружать заранее)
  • maxBufferPx: максимальный буфер загрузки (сколько элементов держать в DOM после скролла)
<cdk-virtual-scroll-viewport
  itemSize="50"
  minBufferPx="200"
  maxBufferPx="400"
  class="viewport">
  ...
</cdk-virtual-scroll-viewport>

5. Работа с элементами разной высоты

Для элементов переменной высоты используйте autosize:

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

@Component({...})
export class MyComponent {
  @ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;

  ngAfterViewInit() {
    this.viewport.setRenderedRange({start: 0, end: 10});
  }
}

И в шаблоне:

<cdk-virtual-scroll-viewport [itemSize]="undefined" class="viewport">
  <div *cdkVirtualFor="let item of items; let i = index"
       [style.height.px]="getItemHeight(i)">
    {{item}}
  </div>
</cdk-virtual-scroll-viewport>

6. Кастомный trackBy

Для оптимизации используйте trackBy как в обычном ngFor:

<div *cdkVirtualFor="let item of items; trackBy: trackByFn">
  {{item.name}}
</div>
trackByFn(index: number, item: any): number {
  return item.id;
}

7. Бесконечный скроллинг

Комбинация виртуального скроллинга с пагинацией:

@Component({...})
export class MyComponent {
  items = [];
  loading = false;
  totalItems = 1000;

  @ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;

  constructor() {
    this.loadItems(0, 20);
  }

  ngAfterViewInit() {
    this.viewport.scrolledIndexChange.subscribe(() => {
      if (this.loading) return;

      const end = this.viewport.getRenderedRange().end;
      const total = this.viewport.getDataLength();

      if (end === total && total < this.totalItems) {
        this.loadMore();
      }
    });
  }

  loadMore() {
    this.loading = true;
    this.loadItems(this.items.length, 20).then(() => {
      this.loading = false;
    });
  }
}

Альтернативные решения

1. ngx-virtual-scroller

npm install ngx-virtual-scroller

Пример использования:

<virtual-scroller [items]="items" [parentScroll]="scroll.window">
  <div *ngFor="let item of scroll.viewportItems">
    {{item}}
  </div>
</virtual-scroller>

2. Реализация вручную

Пример простой ручной реализации:

@Component({
  selector: 'app-custom-virtual-scroll',
  template: `
    <div class="viewport" (scroll)="onScroll($event)">
      <div class="phantom" [style.height.px]="totalHeight">
        <div class="content" [style.transform]="transform">
          <div *ngFor="let item of visibleItems" class="item">
            {{item}}
          </div>
        </div>
      </div>
    </div>
  `
})
export class CustomVirtualScrollComponent {
  items = [...Array(1000).keys()];
  itemHeight = 50;
  visibleItemCount = 10;
  totalHeight = this.items.length * this.itemHeight;
  scrollTop = 0;

  get startIndex(): number {
    return Math.floor(this.scrollTop / this.itemHeight);
  }

  get visibleItems(): any[] {
    return this.items.slice(
      this.startIndex,
      this.startIndex + this.visibleItemCount
    );
  }

  get transform(): string {
    return `translateY(${this.startIndex * this.itemHeight}px)`;
  }

  onScroll(event: Event): void {
    this.scrollTop = (event.target as HTMLElement).scrollTop;
  }
}

Оптимизация производительности

  1. Используйте OnPush change detection

    @Component({
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    
  2. Избегайте сложных вычислений в шаблонах

  3. Оптимизируйте стили
    Используйте will-change и contain: strict для элементов списка

  4. Используйте trackBy
    Для минимизации перерисовки элементов

  5. Оптимизируйте обработку событий
    Используйте debounceTime для scroll-событий

fromEvent(this.viewport.elementRef.nativeElement, 'scroll')
  .pipe(debounceTime(50))
  .subscribe(() => {...});

Работа с асинхронными данными

Пример с Observable:

items$: Observable<any[]>;

constructor(private dataService: DataService) {
  this.items$ = this.dataService.getLargeDataSet();
}
<cdk-virtual-scroll-viewport itemSize="50">
  <div *cdkVirtualFor="let item of items$ | async">
    {{item.name}}
  </div>
</cdk-virtual-scroll-viewport>

Резюмируем

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