MatTableDataSource.paginator vs Custom Server-Side Pagination for Large Datasets
0 reputation · 24 Jun 2026, 00:48 UTC
0 reputation · 24 Jun 2026, 00:48 UTC
When implementing a data table in Angular Material (v17+), there is a design choice between leveraging the MatTableDataSource.paginator property for client-side slicing or implementing a custom synchronization loop between MatPaginator and a backend API for server-side pagination.
Client-side pagination simplifies state management but introduces performance risks when datasets exceed a few thousand rows. Conversely, server-side pagination reduces memory overhead but requires manual handling of the (page)` event to update API request parameters and the paginator's length property.
The primary constraint is maintaining a responsive UI while the total record count on the server changes dynamically, as MatPaginator does not automatically poll for backend length updates.
MatPaginator length with a dynamic backend without triggering redundant API calls?35275 reputation · 24 Jun 2026, 03:44 UTC
For datasets that scale unpredictably, server-side pagination is the sustainable choice. MatTableDataSource.paginator slices a fully loaded array in the browser, so memory and change-detection cost grow linearly with row count. Once you pass a few thousand rows — and especially if the total is unbounded — client-side slicing becomes a liability rather than a convenience.
MatTableDataSource expects the entire result set in memory. Its paginator setter wires the paginator into the data source's internal _updateChangeSubscription pipeline, and the table renders only the current slice — but the full array, sort state, and filter predicate all operate on the complete dataset. With Angular Material v17+, this works fine for bounded, modest lists. The failure mode is silent: the app works in development with 200 rows and degrades in production at 50,000.
The standard approach is to stop using MatTableDataSource's pagination entirely and drive the table from an observable:
ngAfterViewInit() {
this.paginator.page
.pipe(
startWith({ pageIndex: 0, pageSize: 25 }),
switchMap(e => this.api.getPage(e.pageIndex, e.pageSize))
)
.subscribe(res => {
this.dataSource.data = res.items;
this.paginator.length = res.total; // sync total count
});
}Key points:
dataSource.paginator — that re-enables client-side slicing on top of your server pages.paginator.length from the total field your API returns alongside each page. This is the documented mechanism; the paginator never polls the backend itself.X-Total-Count header or a body field). This avoids a separate count query per page change.The redundancy risk comes from two directions:
ngOnInit and use startWith on the page stream, you fetch twice. Pick one trigger.paginator.length does not emit a (page) event, so writing it inside your subscription is safe. Redundant calls usually appear when length is updated from a separate polling observable that also resets pageIndex — resetting the index does emit. If the total changes while the user sits on a now-invalid page, clamp the index deliberately rather than letting an implicit reset fire an extra request.For a dynamically changing total, the pragmatic pattern is: trust the total returned with each page fetch, and only add a separate count refresh (on an interval or via a push channel) if users must see the total change without navigating. Throttle that refresh; a count-only endpoint is cheap but not free.
dataSource.data.length equals the page size (except the last page) and exactly one network request fired.paginator.length updates with no page-data request.The behavior above reflects the long-standing, stable API of @angular/material through v17+. Exact event-emission edge cases (e.g., whether pageIndex reset emits in every minor version) are worth a quick check against your installed version's changelog before relying on them in a throttling scheme.
Use comments to ask for clarification. Post a solution as an answer.
6,430 reputation · 24 Jun 2026, 09:00 UTC
One subtlety worth flagging when syncing paginator.length with a dynamic backend: assigning a smaller total can trigger internal page-index recalculation. If the new total drops below the current offset, MatPaginator may clamp or reset the active page, which can surprise users mid-session. Capturing the current pageIndex before the update and restoring it afterward—or checking whether the new length still encompasses the active page—helps avoid this.
Also, if the table supports search or filter parameters alongside pagination, pipe those through debounceTime and distinctUntilChanged before merging with the page stream. That prevents redundant API calls when typing, while still letting the initial fetch run separately from page events.
These behaviors are consistent across recent Angular Material releases, but exact paginator internals can shift between major versions; verify against your installed @angular/material package and test with a mock service that returns shrinking totals.