Efficient Large‑Dataset Tables with Angular Material: Server‑Side Pagination & Sorting
Implement Angular Material’s MatTable with server‑side pagination and sorting to keep your UI fast when handling thousands of rows. Follow the step‑by‑step guide, trade‑off analysis, and verification checklist to build a scalable table.
01 Jun 2026, 05:20 UTC

Why Server‑Side Pagination Matters
When a table contains thousands of rows, rendering every record on the client is slow and memory‑heavy. Angular Material’s MatTable can delegate the heavy lifting to the server: the client only keeps the current page in memory while the backend supplies the data, total count, and sorting logic. This pattern keeps the UI snappy and scales with the data size.
Core Angular Material Components
MatTable– declarative table markup.MatPaginator– UI for page navigation and page size selection.MatSort– clickable column headers that emit sort changes.MatTableDataSource– a convenience data source that can be swapped for a customDataSourceimplementation.
Step‑by‑Step Implementation
- Project Setup
ng new large-table-demo cd large-table-demo ng add @angular/materialImport the required modules in
app.module.ts:import { MatTableModule } from '@angular/material/table'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [ MatTableModule, MatPaginatorModule, MatSortModule, HttpClientModule, // … other imports ], // … declarations, bootstrap }) export class AppModule {} - Backend Contract
The API must return JSON with two keys:
items(array of rows) andtotal(total number of records). Example response:{ "items": [ { "id": 1, "name": "Alice" }, … ], "total": 1250 } - Service Layer
export interface PageResult { items: T[]; total: number; } @Injectable({ providedIn: 'root' }) export class DataService { constructor(private http: HttpClient) {} fetchPage( url: string, pageIndex: number, pageSize: number, sortField: string, sortDirection: string ): Observable<PageResult<T>> { const params = new HttpParams() .set('page', (pageIndex + 1).toString()) // backend expects 1‑based page .set('size', pageSize.toString()) .set('sort', `${sortField},${sortDirection}`); return this.http.get<PageResult<T>>(url, { params }); } } - Component Wiring
In the component, combine paginator and sort streams to trigger a new fetch whenever either changes.
@Component({ selector: 'app-large-table', templateUrl: './large-table.component.html', styleUrls: ['./large-table.component.css'] }) export class LargeTableComponent implements OnInit { displayedColumns: string[] = ['id', 'name', 'email']; dataSource = new MatTableDataSource<any>(); totalItems = 0; @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; constructor(private dataService: DataService) {} ngOnInit() { // Initial load this.loadPage(0, 10, 'id', 'asc'); // Listen for changes this.paginator.page .pipe( startWith({ pageIndex: 0, pageSize: 10 } as PageEvent), withLatestFrom(this.sort.sortChange.pipe(startWith({ active: 'id', direction: 'asc' }))) ) .subscribe(([pageEvent, sortEvent]) => { const sortField = sortEvent.active; const sortDirection = sortEvent.direction || 'asc'; this.loadPage(pageEvent.pageIndex, pageEvent.pageSize, sortField, sortDirection); }); } loadPage(pageIndex: number, pageSize: number, sortField: string, sortDirection: string) { this.dataService .fetchPage<any>('YOUR_API_ENDPOINT', pageIndex, pageSize, sortField, sortDirection) .subscribe(result => { this.dataSource.data = result.items; this.totalItems = result.total; this.paginator.length = result.total; }); } } - Template
<mat-table [dataSource]="dataSource" matSort> <ng-container matColumnDef="id"> <mat-header-cell *matHeaderCellDef mat-sort-header>ID</mat-header-cell> <mat-cell *matCellDef="let row">{{row.id}}</mat-cell> </ng-container> <ng-container matColumnDef="name"> <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <mat-cell *matCellDef="let row">{{row.name}}</mat-cell> </ng-container> <ng-container matColumnDef="email"> <mat-header-cell *matHeaderCellDef mat-sort-header>Email</mat-header-cell> <mat-cell *matCellDef="let row">{{row.email}}</mat-cell> </ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> </mat-table> <mat-paginator [pageSizeOptions]="[10, 25, 50]" showFirstLastButtons></mat-paginator>
Trade‑Offs & Limitations
- Network Latency – every page or sort action triggers a round‑trip. Caching on the client or debouncing can mitigate perceived slowness.
- Backend Contract – the API must expose page, size, and sort parameters and return the total count; otherwise the paginator cannot compute page numbers.
- Large
pageSizeOptions– an overly long list can clutter the UI; choose a few sensible defaults (e.g., 10, 25, 50). - MatTableDataSource vs Custom DataSource –
MatTableDataSourceis convenient but not ideal for server‑side logic. Replacing it with a customDataSourcegives full control over change detection and allows you to expose an observable that the table subscribes to.
Practical Verification Checklist
- Open the app and confirm the table renders the first page of data.
- Change the page via the paginator and verify the HTTP request URL contains the correct
pageandsizequery params. - Click a column header to sort; ensure the request includes
sort=id,desc(or the selected field). - Inspect the paginator’s
lengthproperty; it should match thetotalreturned by the API. - Resize the browser to test responsiveness and confirm columns adjust correctly.
Actionable Takeaway
For any Angular app that needs to display large tables, wire MatPaginator and MatSort to your backend API. Keep the client lightweight, expose the total count, and consider debouncing rapid user interactions. If you hit performance limits, move to a custom DataSource that streams data and handles caching.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.