Handling Large Datasets in Angular Material: Moving MatTable to the Server
Stop loading thousands of rows into the browser. Learn how to implement a custom DataSource for Angular Material tables to handle pagination and sorting on the server.
05 Apr 2026, 07:34 UTC

The Client-Side Performance Wall
Angular Material's MatTableDataSource is convenient for small datasets. You pass it an array, and it handles filtering, sorting, and pagination automatically. However, once your dataset grows to thousands of rows, this approach creates a performance wall. Loading massive JSON payloads causes browser memory spikes, and the UI becomes sluggish as the DOM struggles to manage the data overhead.
The solution is to shift the heavy lifting to your backend. Instead of downloading the entire table, the application should request only the specific slice of data needed for the current view. This requires moving away from MatTableDataSource and implementing a custom DataSource class.
Architecting the Server-Side DataSource
To implement server-side logic, you must create a class that extends the DataSource base class. This allows the mat-table to remain agnostic about where the data comes from while giving you full control over the API requests.
The custom data source acts as a bridge. It listens for changes from the MatPaginator (page index and page size) and the MatSort (column key and direction), then triggers a new HTTP request to your REST endpoint with these values as query parameters.
The Communication Flow
- MatPaginator: Emits a
pageevent when the user changes the page or size. - MatSort: Emits a
sortChangeevent when a header is clicked. - Custom DataSource: Captures these events and calls the backend service.
- Backend: Executes a SQL
OFFSETandLIMITquery with anORDER BYclause.
Implementation Example
Below is a structural example of how to connect a custom data source to a backend service. This assumes you are using Angular Material v15+.
// server-data-source.ts
import { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable, combineLatest }
import { HttpClient } from '@angular/common/http';
export class ServerDataSource extends DataSource<any> {
private _dataStream = new BehaviorSubject<any[]>([]);
private _sortStream = new BehaviorSubject<any>({ active: '', direction: '' });
private _paginatorStream = new BehaviorSubject<any>({ pageIndex: 0, pageSize: 10 });
constructor(private http: HttpClient) { super(); }
connect(): Observable<any[]> {
return combineLatest([
this._sortStream,
this._paginatorStream
]).pipe(
switchMap(([sort, paginator]) => {
const params = {
offset: (paginator.pageIndex * paginator.pageSize).toString(),
limit: paginator.pageSize.toString(),
sort: sort.active,
dir: sort.direction
};
return this.http.get<any>('/api/data', { params });
}),
map(response => response.items)
);
}
disconnect(): void {
this._dataStream.complete();
}
setSort(sort: any) { this._sortStream.next(sort); }
setPage(page: any) { this._paginatorStream.next(page); }
}
Integrating with the Component
In your component, you must link the UI components to the data source methods. Run these assignments in the ngOnInit or via event bindings in the template:
- Paginator: Bind to
(page)="dataSource.setPage($event)". - Sort: Bind to
(matSortChange)="dataSource.setSort($event)".
Trade-offs and Limitations
Moving pagination to the server introduces a specific UX challenge: State Reset. By default, if a user is on page 10 and changes the sort order, the backend returns the first page of the newly sorted data. If your application doesn't explicitly reset the MatPaginator index to 0, the user may be left on page 10 of a result set that is now shorter, leading to an empty table view.
Additionally, server-side sorting removes the ability to perform instant, local filtering. Any search or filter criteria must now be passed as additional query parameters to the API, increasing the number of network round-trips.
Verifying the Implementation
To ensure the implementation is functioning correctly, use the Browser DevTools Network tab:
- Click a sortable column header. Verify that a new XHR request is sent with the
sortanddirparameters. - Change the page size. Verify that the
limitparameter updates and the total count of returned items matches the new size. - Confirm that the
offsetis calculated aspageIndex * pageSize(e.g., Page 2 with a size of 20 should send an offset of 20).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.