← Retour au blog
virtual-scrollAngularperformanceCDKDOM-optimization

Virtual Scroll : afficher 10 000 items sans tuer le navigateur

Le scrolling de listes massives est un classique des problèmes de performance web. Afficher 10 000 items dans un simple *ngFor tue littéralement le navigateur : le DOM explose, le repaint devient un cauchemar, et l'utilisateur observe un écran gelé pendant plusieurs secondes. Le virtual scroll résout ce problème en ne rendant que les items visibles à l'écran, plus quelques tampon avant et après pour éviter les flashs blancs lors du défilement. C'est une technique époustouflante quand elle est bien maîtrisée, et franchement indispensable dès qu'on dépasse quelques centaines d'items.

Angular Material et le CDK (Component Dev Kit) fournissent un module ScrollingModule prêt à l'emploi, basé sur le pattern du virtual scrolling classique. L'idée est simple : au lieu de créer un DOM node pour chaque item, on crée un conteneur avec la hauteur totale théorique, et on translate/positionne le bloc visible selon la position du scroll. Quand vous scrollez, les calculs sont rapides (juste de la géométrie), et seuls les items du viewport se montent/démontent du DOM. Performance garantie même avec 50 000 items.

Implémenter le virtual scroll en Angular

Le setup est trivial avec le CDK. D'abord, importer le ScrollingModule depuis @angular/cdk/scrolling. Ensuite, remplacer le *ngFor classique par cdk-virtual-scroll-viewport et *cdkVirtualFor. Voici un exemple concret :

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} ,

}));

}

Le paramètre itemSize est critique : c'est la hauteur fixe de chaque item en pixels. Le viewport ajuste sa hauteur et gère le scrolling. Sans cette hauteur fixe, le CDK ne peut pas calculer correctement les positions et le virtual scroll s'effondre. C'est d'ailleurs le piège numéro un : oublier de fixer itemSize ou utiliser des hauteurs variables sans *cdkVirtualFor.

Gérer les hauteurs variables et le scrolling dynamique

Malheureusement, la vie réelle ne se limite pas à des items de 50px identiques. Les cartes, les posts, les commentaires ont des hauteurs variables. Le CDK supporte cela via la stratégie CdkVirtualScrollStrategy et un callback estimateScrollOffset. Mais c'est lourd. Une approche pragmatique : utiliser une hauteur estimée (itemSize), et laisser le CDK se recalibrer via des observations du DOM. Ou, mieux encore, normaliser les hauteurs avec du CSS (truncate, ellipsis) et fixer une hauteur max.

Exemple avec hauteur variable gérée par le 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 {

// Retourner la hauteur réelle de l'item

return item.expanded ? 300 : 80;

}

Avec itemSize undefined, le CDK bascule en mode mesure dynamique : il observe chaque item et ajuste son calcul. C'est moins rapide que itemSize fixe, mais acceptable pour quelques milliers d'items. Au-delà, pensez à virtualiser aussi les groupes ou implémenter un lazy-load.

Intégration avec le lazy-loading et l'infini scroll

Un vrai cas d'usage : afficher 100 items, et charger 100 de plus au fur et à mesure du scroll. Combiner virtual scroll + pagination infinie. Écouter l'event scrolledIndexChange et charger quand l'index approche la fin. Exemple :

@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;

});

}

}

Cette approche scaling vraiment bien : vous gardez ~200 items en DOM (les visibles + buffer), et les données en mémoire augmentent progressivement. Utilisateurs heureux, pas de GC pressure excessive.

Les pièges courants et comment les éviter

Le premier piège : oublier itemSize ou le mal configurer. Si itemSize ne correspond pas à la hauteur réelle, le scroll devient chaotique, les items flashent, et l'UX est pourrie. Solution : mesurer la hauteur réelle d'un item (DevTools, inspecteur) et la fixer en dur. Le deuxième piège : forcer un changement de détection à chaque scroll. Le virtual scroll est déjà très réactif au scroll ; ajouter (scroll)="onScroll()" avec un ChangeDetectionStrategy.Default tue la perf. Utilisez OnPush et écoutez scrolledIndexChange pour les actions métier. Le troisième piège : penser que virtual scroll résout tout. Si chaque item exécute du code coûteux (appels API, calculs lourds), le virtual scroll ne y change rien. Optimisez le rendu de l'item lui-même.

Mesurer et déboguer la performance

Utilisez Chrome DevTools : Timeline tab, enregistrez un scroll et cherchez les jank (frames > 16ms). Avec virtual scroll, vous devriez voir des frames stables autour de 60 FPS. Si ce n'est pas le cas, inspectez le contenu de chaque item : est-ce qu'il y a des animations CSS lourdes, des images non-optimisées, du change detection excessif ? Utilisez aussi ngIf avec trackBy pour éviter les re-créations de composants. Le *cdkVirtualFor supporte trackBy comme un *ngFor classique.

Conclusion

Virtual scroll n'est pas une magie, c'est une technique d'optimisation DOM fondamentale. Avec le CDK et une bonne compréhension de itemSize, du lazy-loading et de l'architecture du rendu, vous pouvez afficher 10 000+ items sans ralentir. La clé : fixer les hauteurs, écouter les changements de scroll pour charger, et profiler régulièrement. Une liste fluide, c'est la différence entre une app qui se sent responsive et une qui se sent lente.

Développeur Angular & Mobile freelance — Strasbourg.

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