Angular Material MatTable with Virtual Scrolling and Server‑Side Pagination: Performance, Accessibility, and Theming
Explore how Angular Material’s MatTable with virtual scrolling and server‑side pagination solves performance bottlenecks for large datasets while preserving accessibility and visual consistency.
08 Dec 2025, 07:27 UTC

The performance challenge: DOM size and jank
When a table displays thousands of rows, the browser must render every row as a separate DOM node. Each node consumes memory, triggers layout calculations, and can cause jank during scrolling. The result is a sluggish interface that feels unresponsive, especially on low‑end devices. In addition, screen‑reader users may encounter focus order problems when the DOM changes dramatically during pagination.
Why the DOM becomes a bottleneck
The default MatTable implementation creates a row element for every item in the data source. Even if you bind a large array, Angular Material still iterates over the entire collection to generate the markup. This “full‑render” approach works for a few dozen rows but quickly becomes a performance problem at scale. The browser’s rendering pipeline, CSS layout, and JavaScript change detection all need to process each node, leading to noticeable lag when the user scrolls.
Virtual scrolling: rendering only visible rows
Virtual scrolling works by swapping the visible portion of the table’s data source as the user scrolls. Internally, the cdkVirtualScrolling directive monitors the scroll position and re‑uses a small pool of row components. Because only a handful of rows exist in the DOM at any moment, layout recalculations and change detection run far less often. The performance gain is measurable: tests with 50 000 rows show smooth 60 fps scrolling on mid‑range laptops.
Server‑side pagination: fetching only needed data
Instead of loading the entire dataset once, you can request a page of data from the server on demand. The table emits pageIndexChange when the user moves to a new page and pageSizeChange when the page size is adjusted. Your component listens to these events, builds the request parameters (offset and limit), calls the API, and updates the data source. This pattern ensures that only the needed slice travels over the network, which is crucial for mobile connections or large datasets.
Worked example and practical implementation
Below is a concise example that demonstrates the core pieces. Adjust the placeholders to match your API contract.
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { DataService } from './data.service'; // your HTTP wrapper
export interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-table',
template: `
#
{{element.id}}
Name
{{element.name}}
Email
{{element.email}}
Load next page
`,
styles: [`
table { width: 100%; }
`]
})
export class UserTableComponent implements OnInit {
displayedColumns = ['id', 'name', 'email'];
dataSource = new MatTableDataSource([]);
pageSize = 10;
pageIndex = 0;
@ViewChild(MatDialog) dialog: MatDialog; // example for accessibility
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.loadPage(); // initial load
}
loadPage(): void {
const offset = this.pageIndex * this.pageSize;
this.dataService.getUsers(offset, this.pageSize).subscribe(response => {
this.dataSource.data = response.items;
});
}
// optional: handle pagination changes directly on the table
onPageChanged(event: any) {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadPage();
}
}
- The
dataSourceis aMatTableDataSourcethat wraps the array returned from the API. cdkVirtualScrollingis applied automatically when you add thecdk-virtual-scrolldirective to the container element (not shown for brevity). In practice you would wrap thetablein acdk-virtual-scrollelement.- Pagination events are captured via the
pageIndexChangeandpageSizeChangeoutputs of theMatPaginatorcomponent; the example uses a manual button for simplicity. MatDialogis included to illustrate built‑in focus management; you can open a detail view for each row without worrying about focus traps.
Trade‑offs and limitations
- Version requirement: virtual scrolling was introduced in Angular Material 14. Versions older than 14 will not compile the
cdkVirtualScrollingmodule, so runng versionfirst and upgrade if needed. - Focus management: repeatedly opening and closing
MatDialoginstances can create focus traps if the dialog is not properly closed or if you forget to return focus to the originating element. Always use theclosemethod and consider restoring focus in theafterClosedcallback. - Theming conflicts: overriding default CSS variables in a custom theme may interfere with future Material updates. Test your theme against a fresh Material release to ensure no breaking changes.
Verification steps before production
- Run
ng versionin your project root. Verify that @angular/material is at least version 15 (or the minimum required for virtual scrolling in your Angular version). - Generate a new component:
ng generate component table. Ensure you have theMatTableModule,CdkVirtualScrollingModule, andMatPaginatorModuleimported in the module. - Open Chrome DevTools, select the
Elementspanel, and inspect the table’s DOM while scrolling. You should see only a few<tr>elements corresponding to the visible rows; the rest are not rendered. - Use the Network tab to confirm that each pagination request returns only the expected number of records (e.g., 10 items per request).
Actionable closing
To adopt virtual scrolling and server‑side pagination in your Angular Material tables:
- Confirm your Angular Material version meets the minimum requirement.
- Import the necessary modules (
MatTableModule,CdkVirtualScrollingModule,MatPaginatorModule) in the feature module. - Wrap the table markup in a
cdk-virtual-scrollcontainer or enable virtual scrolling via thescrollableinput. - Listen to pagination events, compute the API offset, and refresh the data source.
- Test with a realistic data set (tens of thousands of rows) and verify smooth scrolling and correct network requests.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.