← Retour au blog
virtual-scrollAngularperformanceCDKDOM-optimization

Virtual Scroll: Rendering 10,000 Items Without Breaking the Browser

Rendering thousands of items in a simple *ngFor is a classic performance disaster. Ten thousand items means DOM explosion, repaints become nightmarish, and users watch a frozen screen for seconds. Virtual scroll fixes this by rendering only visible items plus a small buffer above and below to prevent white flashes during scroll. It's a powerful technique when done right, and honestly essential once you exceed a few hundred items.

Angular Material and the CDK (Component Dev Kit) provide a ready-to-use ScrollingModule based on the virtual scrolling pattern. The concept is simple: instead of creating a DOM node for each item, you create a container with the theoretical total height, then translate/position the visible block based on scroll position. As you scroll, calculations are fast (just geometry), and only viewport items mount/unmount from the DOM. Performance is guaranteed even with 50,000 items.

Implementing Virtual Scroll in Angular

Setup is trivial with the CDK. First, import ScrollingModule from @angular/cdk/scrolling. Then replace the classic *ngFor with cdk-virtual-scroll-viewport and *cdkVirtualFor. Here's a concrete example:

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

@Component({

selector: 'app-large-list',

template: `

<cdk-virtual-scroll-viewport itemSize="50" class="list-viewport">

<div *cdkVirtualFor="let item of items" class="list-item">

{{ item.name }} - {{ item.id }}

</div>

</cdk-virtual-scroll-viewport>

`,

styles: [`

.list-viewport {

height: 600px;

border: 1px solid #ccc;

}

.list-item {

height: 50px;

padding: 10px;

border-bottom: 1px solid #eee;

}

`]

})

export class LargeListComponent {

items = Array.from({ length: 10000 }, (_, i) => ({

id: i,

name: Item ${i} ,

}));

}

The itemSize parameter is critical: it's the fixed height of each item in pixels. The viewport adjusts its height and manages scrolling. Without this fixed height, the CDK cannot calculate positions correctly and virtual scroll collapses. This is pitfall number one: forgetting itemSize or using variable heights without properly configuring *cdkVirtualFor.

Handling Variable Heights and Dynamic Scrolling

Real-world scenarios rarely stick to identical 50px items. Cards, posts, comments have variable heights. The CDK supports this via CdkVirtualScrollStrategy and an estimateScrollOffset callback. But it's heavy. A pragmatic approach: use an estimated height (itemSize) and let the CDK recalibrate via DOM observations. Or better, normalize heights with CSS (truncate, ellipsis) and fix a max-height.

Example with variable height managed by the CDK:

<cdk-virtual-scroll-viewport [itemSize]="undefined" (scrolledIndexChange)="onScrollChange($event)" class="viewport">

<div *cdkVirtualFor="let item of items; let i = index" [style.height.px]="getItemHeight(item)" class="item">

{{ item.content }}

</div>

</cdk-virtual-scroll-viewport>

getItemHeight(item: any): number {

// Return the real height of the item

return item.expanded ? 300 : 80;

}

With itemSize undefined, the CDK switches to dynamic measurement mode: it observes each item and adjusts calculations. It's slower than fixed itemSize, but acceptable for a few thousand items. Beyond that, consider virtualizing groups or implementing lazy-load.

Integration with Lazy-Loading and Infinite Scroll

A real use case: display 100 items, then load 100 more as you scroll. Combine virtual scroll + infinite pagination. Listen to scrolledIndexChange and load when the index approaches the end. Example:

@Component({

selector: 'app-infinite-list'

})

export class InfiniteListComponent implements OnInit {

items: any[] = [];

pageSize = 100;

currentPage = 0;

isLoading = false;

constructor(private api: ApiService) {}

ngOnInit() {

this.loadMore();

}

onScrollChange(index: number) {

if (index > this.items.length - 50 && !this.isLoading) {

this.loadMore();

}

}

loadMore() {

this.isLoading = true;

this.api.getItems(this.currentPage, this.pageSize).subscribe((newItems) => {

this.items.push(...newItems);

this.currentPage++;

this.isLoading = false;

});

}

}

This approach truly scales: you keep ~200 items in the DOM (visible plus buffer), and data in memory grows progressively. Happy users, no excessive GC pressure.

Common Pitfalls and How to Avoid Them

First pitfall: forgetting itemSize or misconfiguring it. If itemSize doesn't match real height, scrolling becomes chaotic, items flash, and UX suffers. Solution: measure the actual height of one item (DevTools, inspector) and hardcode it. Second pitfall: forcing change detection on every scroll. Virtual scroll is already reactive to scroll; adding (scroll)="onScroll()" with ChangeDetectionStrategy.Default kills performance. Use OnPush and listen to scrolledIndexChange for business logic. Third pitfall: thinking virtual scroll solves everything. If each item runs expensive code (API calls, heavy calculations), virtual scroll changes nothing. Optimize the item rendering itself.

Measuring and Debugging Performance

Use Chrome DevTools: Timeline tab, record a scroll and look for jank (frames > 16ms). With virtual scroll, you should see stable frames around 60 FPS. If not, inspect each item's content: are there heavy CSS animations, unoptimized images, excessive change detection? Also use ngIf with trackBy to avoid component recreation. The *cdkVirtualFor supports trackBy like a classic *ngFor.

Conclusion

Virtual scroll is not magic, it's a fundamental DOM optimization technique. With the CDK and a solid understanding of itemSize, lazy-loading, and rendering architecture, you can display 10,000+ items without slowdown. The key: fix heights, listen to scroll changes for loading, and profile regularly. A smooth list is the difference between an app that feels responsive and one that feels sluggish.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️