Architecture note: Using PrimeNG p-table with lazy loading and virtual scrolling for large datasets
Guide for configuring PrimeNG p-table with lazy loading and virtual scrolling to handle large datasets safely and efficiently.
20 Aug 2025, 03:48 UTC

Requirements
The table must display tens of thousands of rows without freezing the UI. It needs server‑side paging, sorting, and filtering, a visible loading indicator while data is fetched, and it must keep the main thread free for user interactions.
Smallest suitable design
Configure PrimeNG’s p-table with lazy loading and virtual scrolling enabled:
<p-table
[lazy]="true"
[virtualScroll]="true"
[itemSize]="50"
(onLazyLoad)="loadData($event)"
[value]="data"
[totalRecords]="totalRecords"
>
<ng-template pTemplate="header">
<tr>
<th *ngFor="let col of columns">{{col.header}}</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-rowData>
<tr>
<td *ngFor="let col of columns">{{rowData[col.field]}}</td>
</tr>
</ng-template>
</p-table>
In the component implement loadData to call a backend service, passing the pagination, sort and filter objects that PrimeNG supplies:
loadData(event: LazyLoadEvent): void {
this.loading = true;
this.dataService
.fetchPage({
first: event.first ?? 0,
rows: event.rows ?? 50,
sortField: event.sortField,
sortOrder: event.sortOrder,
filters: event.filters ?? {}
})
.pipe(
catchError(err => {
this.loading = false;
this.error = err;
return of({data: [], totalRecords: 0});
})
)
.subscribe(resp => {
this.data = resp.data;
this.totalRecords = resp.totalRecords;
this.loading = false;
});
}
Trust / data boundaries
Column definitions and template markup are trusted because they are written by developers. Raw data arriving from the backend must never be trusted for rendering: escape HTML or use Angular’s DomSanitizer / safe pipe before inserting into the DOM, and validate the payload shape (e.g., ensure data is an array and each row contains the expected fields) before assigning to this.data.
Operational checks
- Bind a loading flag to a
p-progressBaror overlay so users see activity whileloadDatais pending. - Log the timestamp and duration of each
onLazyLoadcall to monitor latency. - Implement retry with exponential backoff in the service layer for transient network failures.
- Ensure change detection runs after the data arrives; if the service call is outside Angular’s zone, wrap the assignment in
ngZone.run(() => { … }).
Failure modes
- A slow or malformed backend response can leave the table stuck in the loading state, hiding data indefinitely.
- If
itemSizedoes not match the actual rendered row height (including padding, borders, dynamic content), virtual scrolling miscalculates offsets, causing jerky scroll or blank gaps. - Updating the table’s data array outside the
loadDatahandler (e.g., via a separate subscription) can trigger Angular’sExpressionChangedAfterItHasBeenCheckedError.
Design change triggers
- When the dataset shrinks to a few hundred rows, drop lazy/virtual scrolling and use simple client‑side pagination for lower complexity.
- If row expansion or tree hierarchy becomes necessary, migrate to
p-treeTablewhich supports lazy loading natively. - For offline‑first requirements, cache fetched pages in IndexedDB within the service layer and serve from that cache when the network is unavailable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.