Vous avez une liste de 10 000 produits, messages ou logs à afficher. Mettre tout en DOM tue le navigateur : 10 000 nœuds, c'est des mégaoctets de mémoire, des calculs de layout qui bloquent le thread principal, et une scroll expérience catastrophique. Le virtual scroll (ou windowing) est la solution : ne rendre que les items visibles à l'écran, plus un buffer autour. Mais c'est un outil traître. Beaucoup de développeurs Angular collent un cdkVirtualScrollViewport et croient que c'est fini. Trois jours plus tard, le scroll saccade, la mémoire monte, et ils ne comprennent pas pourquoi.
Le virtual scroll n'est pas magique
Le CDK Virtual Scroll de Material fonctionne en deux phases. D'abord, il calcule quelle tranche d'items est visible selon la position du scroll. Ensuite, il met à jour le DOM pour afficher uniquement cette tranche. C'est efficace, mais le dimensionnement du buffer et la détection des changements sont critiques. Un buffer trop petit crée un effet de flash quand on scroll vite : Angular a pas le temps de rendre les items suivants avant qu'ils deviennent visibles. Un buffer trop gros consomme de la mémoire et des cycles CPU inutilement. Il n'y a pas de valeur universelle. Sur un écran 1080p avec des items de 50px, vous pouvez afficher ~20 items à la fois. Un buffer de 500px de chaque côté (5-10 items) suffit souvent. Sur mobile ou avec des items plus gros, réduisez. La vraie astuce est de mesurer.
Configurer le buffer et l'item size
Dans un template, cela ressemble à : <cdk-virtual-scroll-viewport itemSize="50" minBufferPx="500" maxBufferPx="1000" [items]="items"> . L' itemSize doit être la hauteur en pixels d'un seul item. Si les items ont des hauteurs variables (ce qui arrive souvent en vrai), vous avez deux options : soit utiliser scrolledIndexChange pour calculer dynamiquement, soit garder une hauteur moyenne et accepter un peu de scintillement. minBufferPx est la zone de sécurité : si moins de 500px de contenu caché reste à rendre, Angular prépare les items suivants. maxBufferPx limite le travail : il ne rendra jamais plus d'1 Mo de buffer. L'ordre compte : minBuffer petit, maxBuffer gros, sinon le scroll devient saccadé. Un piège courant : définir itemSize à zéro ou ne pas le définir du tout. Le viewport ne peut pas calculer combien d'items tenir à l'écran sans cette info, et tout se casse silencieusement.
Change Detection : le tueur silencieux
Ici vient la partie méchante. Imaginez que vous faites *ngFor="let item of items; trackBy: trackByFn" dans le template du virtual scroll. Chaque fois que la liste change, Angular doit vérifier si les items affichés ont changé. Avec une stratégie Default , cela peut déclencher une vérification complète du composant à chaque scroll. Utilisez ChangeDetectionStrategy.OnPush systématiquement avec virtual scroll. Cela signifie qu'Angular ne regarde le composant que si une Input change ou un événement observable émet. Couplé à un trackBy efficace, c'est la combinaison gagnante. Voici un exemple : @Component({ selector: 'app-item-list', changeDetection: ChangeDetectionStrategy.OnPush }); export class ItemListComponent { @Input() items: Item[]; trackByFn(index: number, item: Item) { return item.id; } } . Le trackBy retourne une clé unique pour chaque item. Sans cela, Angular recrée le DOM pour chaque élément même s'ils n'ont pas changé. Avec OnPush et trackBy, vous divisez les appels de change detection par 10 ou plus.
Gestion mémoire et streaming de données
10 000 items en mémoire, c'est 10-50 Mo selon la structure. Sur un navigateur desktop, c'est acceptable. Sur mobile, c'est serré. La meilleure approche est le lazy loading ou le streaming : charger les items par batch de 100-500 selon le scroll. RxJS et le CDK Virtual Scroll jouent bien ensemble. Écoutez scrolledIndexChange pour détecter quand l'utilisateur approche de la fin, puis chargez les items suivants. Exemple schématique : scrolledIndexChange.pipe( filter(index => index > items.length - 100), switchMap(() => this.api.loadMore()), tap(newItems => this.items = [...this.items, ...newItems]) ).subscribe() . Attention : chaque fois que vous modifiez la liste (ajout, suppression), le virtual scroll recalcule l'offset. Si vous faites des mutations directes sur l'array ( items.push(...) ) sans notifier Angular, le viewport perd la synchronisation et le scroll saute. Toujours utiliser une nouvelle référence : this.items = [...this.items, ...newItems] .
Un piège courant : les observables dans le template
Beaucoup écrivent <cdk-virtual-scroll-viewport [items]="items$ | async"> . Cela crée un nouveau binding à chaque change detection, ce qui annule les bénéfices du OnPush. Mieux : résolvez l'observable dans le composant avec toSignal() (Angular 16+) ou un Subject classique. Gardez la source de vérité en TypeScript, pas dans le template. Autre piège : les items contiennent des objets complexes avec des getters ou des pipes coûteux. Chaque fois qu'un item entre dans le viewport, ces calculs s'exécutent. Précalculez et stockez les valeurs avant le rendering, ou utilisez memo/cache si vous ne pouvez pas faire autrement.
Conclusion : mesurer, configurer, itérer
Optimiser un virtual scroll de 10 000 items, c'est un triathlon : dimensionner le buffer sans empirique, utiliser OnPush + trackBy sans faille, et streamer les données pour ne jamais charger tout en mémoire. Il n'y a pas de recette unique. Mesurez avec Chrome DevTools (Profiler, Memory), testez sur des vrais appareils (un mobile fait mal aux performances), et ajustez. Un scroll fluide à 60 fps sur 10 000 items est totalement possible, mais ça demande un peu de rigueur.