CDK Virtual Scrolling in Angular Material: Rendering Large Lists Without the Memory Tax
CDK Virtual Scrolling renders only visible items plus a buffer, keeping DOM node count constant even for 10,000-row lists. This blog covers viewport mechanics, fixed vs. variable item sizing, infinite scroll integration, SSR caveats, and a verification checklist you can run in DevTools today.
01 Sept 2026, 00:45 UTC

The Problem: DOM Explosion in Large Lists
Rendering a list of 10,000 items with *ngFor creates 10,000 DOM nodes. The browser spends cycles on layout, style calculation, and paint for every node — even those far outside the viewport. Users perceive jank during scroll, memory climbs, and on mobile the tab may crash. Angular Material’s CDK Virtual Scrolling attacks this by keeping only the visible items (plus a small buffer) in the DOM.
How the Viewport Controls Rendering
The <cdk-virtual-scroll-viewport> component from @angular/cdk/scrolling acts as a window into a larger virtual list. It measures its own height, calculates which items intersect the viewport plus the configured buffer (minBufferPx/maxBufferPx, default 200 px each), and instantiates only those item templates. As the user scrolls, items leaving the buffer are destroyed and new ones are created — recycling the same template instances when possible.
Item size is the contract the viewport relies on. For fixed-height rows you set itemSize (in pixels) on the viewport. For variable heights you must supply a VirtualScrollStrategy (the built-in FixedSizeVirtualScrollStrategy works for uniform items; custom strategies measure each item via a ResizeObserver when you add the autosizeVirtualScroll directive).
Integration Patterns: Data Sources and Infinite Scroll
The viewport accepts an array, an Observable, or a DataSource. For infinite scrolling, listen to the viewport’s scrolledIndexChange or elementScrolled events. When the reported index approaches the end of the current data, append the next page. Because the viewport only renders a slice, the appended items become eligible for rendering immediately without a full reflow.
Worked Example: 10,000 Fixed-Height Rows
<!-- app-virtual-list.component.html -->
<cdk-virtual-scroll-viewport itemSize="48" class="viewport">
<div *cdkVirtualFor="let item of items; templateCacheSize: 10" class="row">
{{ item.id }} — {{ item.label }}
</div>
</cdk-virtual-scroll-viewport>
/* app-virtual-list.component.css */
.viewport { height: 100vh; width: 100%; }
.row { height: 48px; line-height: 48px; padding: 0 16px; border-bottom: 1px solid #eee; }
// app-virtual-list.component.ts
import { Component, OnInit } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
selector: 'app-virtual-list',
templateUrl: './virtual-list.component.html',
styleUrls: ['./virtual-list.component.css'],
standalone: true,
imports: [CdkVirtualScrollViewport]
})
export class VirtualListComponent implements OnInit {
items = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
label: `Item ${i + 1}`
}));
ngOnInit() {}
}
Open DevTools → Elements tab while scrolling. You’ll see roughly viewportHeight / 48 + buffer/48 <div class="row"> nodes — typically 30–50 — not 10,000. Heap snapshots in the Memory panel show a stable detached node count, whereas *ngFor grows linearly.
Trade-offs and Limitations
- Variable heights add complexity. The
autosizeVirtualScrolldirective uses aResizeObserverper item; measurement overhead can offset gains if items are extremely small or numerous. Incorrect measurements cause scroll jumps or blank gaps. - Nested scrolling breaks calculations. The viewport must be the primary scrollable ancestor. Placing it inside another scrolling container (e.g., a sidebar with
overflow:auto) leads to incorrect visible-range math. - SSR renders everything first. During server-side rendering Angular outputs all 10,000 items. Virtualization activates only after hydration. In Angular 17+ you can defer the viewport with
@defer (on viewport)or wrap it in an*ngIfthat becomes true after hydration. - Focus management on recycled items. Keyboard navigation and ARIA roles (
listbox/option) work because only live items exist, but restoring focus after a route change requires storingviewport.measureScrollOffset()and callingviewport.scrollToOffset()on return.
Verification Checklist Before Shipping
- Inspect DOM node count in DevTools while scrolling a 10k list — expect constant count.
- Profile heap snapshots: detached nodes should stay flat.
- Run Chrome Performance tab during fast scroll; target 60 fps, no long tasks >50 ms.
- Test keyboard (Tab, Arrow keys) and screen reader (NVDA/VoiceOver) for focus and ARIA.
- Deliberately set a wrong
itemSize, observe scroll jump, then enableautosizeVirtualScrolland confirm smoothness. - View page source (SSR) — all items present; after hydration, only viewport items remain.
Next Steps
Add ScrollingModule to your feature module, wrap the viewport in a container with a constrained height (flex:1 or 100vh), and start with fixed-size items. Measure, then decide if variable-height strategy is worth the extra measurement cost. The performance ceiling for most admin dashboards and feeds is fixed-size virtual scrolling — it’s the lowest-effort, highest-impact win in the CDK toolkit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.