Efficiently Render Large Lists with Angular Material CDK Virtual Scroll
Learn how Angular Material’s CDK Virtual Scroll renders only the visible slice of a huge list, keeping the DOM small and maintaining 60 fps scrolling.
27 Jun 2026, 20:51 UTC

Problem: UI jank when scrolling thousands of rows
When you bind a plain *ngFor to an array with tens of thousands of items, Angular creates a DOM node for each entry. The browser struggles to layout, paint, and handle scroll events, leading to dropped frames and noticeable lag, especially on low‑end devices.
Thesis: CDK Virtual Scroll keeps the DOM constant
Angular Material’s ScrollingModule provides the cdk-virtual-scroll-viewport directive. It renders only the subset of items that are currently visible (plus a small buffer) and reuses the same DOM nodes as the user scrolls. This keeps the DOM size bounded, maintains ~60 fps, and reduces memory pressure.
How it works
The viewport measures the scroll offset, calculates which index range should be visible based on an item height (fixed or dynamic), and swaps out the data bound to a recycled set of element templates. For fixed‑size items you supply a pixel value via itemSize; for variable sizes you pass a function that returns the height of a given index, enabling the autosize feature.
Worked example: Fixed‑height list
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, ScrollingModule],
bootstrap: [AppComponent]
})
export class AppModule {}
{{ item.id }}: {{ item.name }}
// large-list.component.ts
import { Component, OnInit } from '@angular/core';
interface Row { id: number; name: string; }
@Component({
selector: 'app-large-list',
templateUrl: './large-list.component.html',
styleUrls: ['./large-list.component.scss']
})
export class LargeListComponent implements OnInit {
items: Row[] = [];
ngOnInit() {
// Simulate a large dataset; in a real app this could come from a service
for (let i = 0; i < 50000; i++) {
this.items.push({ id: i, name: `Item ${i}` });
}
}
}
Place the component in your template, run ng serve, and open Chrome DevTools → Performance → FPS meter. While scrolling, you should see the frame rate stay near 60 fps and the Elements panel show only a limited number of div.item nodes (approximately viewport height / 50 px + buffer).
Trade‑off / Limitation: Item size must be known
If you cannot guarantee a fixed height, use the autosize feature:
{{item.name}}
g etItemSize(index: number): number {
// Example: return a measured height; in practice you might cache results
return 40 + (index % 3) * 10; // varies between 40 and 60px
}
Autosize adds a small measurement overhead because the directive must read the actual height of each newly rendered item. It also requires Angular CDK v11 or later for stable behavior; older versions may need a fixed height fallback.
Practical verification steps
- Build a fresh Angular CLI project:
ng new virtual-demo --style=scss --strict=false - Add dependencies:
ng add @angular/material @angular/cdk(accept defaults). - Import
ScrollingModuleinAppModule. - Insert the component code above, serve with
ng serve. - Open DevTools, enable the FPS meter, and scroll. Confirm stable FPS and a bounded DOM node count.
- Optionally, deliberately set an incorrect
itemSize(e.g., 20px when items are 50px) and observe rendering glitches, proving the directive’s reliance on accurate size estimation.
Actionable closing
Start by adding ScrollingModule to your NgModule, wrap your list in cdk-virtual-scroll-viewport, measure an average item height (or use the autosize function), and test with Chrome DevTools’ FPS meter. You’ll gain immediate smoothness for large lists without changing your data‑fetching strategy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.