Guide
PrimeNG DataTable Lazy Loading Architecture for Large Datasets in Angular
Architecture guide for using PrimeNG p-table with lazy loading to display >10k rows in Angular while meeting load‑time and memory constraints.
Published by Tasadduq Burney
26 Apr 2026, 14:01 UTC
4 min55.3K views0

Problem Statement
An Angular application must display tables with more than 10 000 rows while keeping the initial render under two seconds and limiting memory consumption to the rows currently visible plus a small buffer. Users expect responsive filtering, sorting, and paging without noticeable lag.
Requirements
- Initial load time ≤ 2 seconds for an empty table (only UI framework loaded).
- Memory usage bounded to visible rows + a configurable buffer (e.g., 5 rows).
- Backend must return only the slice of data requested by the UI.
- Filtering, sorting, and paging must trigger a fresh data request.
- Errors from the backend must not crash the Angular app; an error state should be shown.
Smallest Suitable Design
Use PrimeNG’s p-table with lazy loading enabled. The component delegates data retrieval to a service method that accepts a LazyLoadEvent and returns a paginated result.
Template
<p-table #dt [value]="rows" [lazy]="true" (onLazyLoad)="loadData($event)"
[totalRecords]="total" [rows]="pageSize" [paginator]="true"
[filters]="filters" filterMatchMode="startsWith">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="id">ID</th>
<th pSortableColumn="name">Name</th>
<th pSortableColumn="value">Value</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr>
<td>{{row.id}}</td>
<td>{{row.name}}</td>
<td>{{row.value}}</td>
</tr>
</ng-template>
</p-table>
Component Logic
import { Component } from '@angular/core';
import { LazyLoadEvent } from 'primeng/api';
import { DataService } from './data.service';
interface PageResult {
data: any[];
total: number;
}
@Component({
selector: 'app-large-table',
templateUrl: './large-table.component.html'
})
export class LargeTableComponent {
rows: any[] = [];
total = 0;
pageSize = 20;
filters: any = {};
constructor(private dataSvc: DataService) {}
loadData(event: LazyLoadEvent): void {
const { first, rows, sortField, sortOrder, filters } = event;
const page = first / rows + 1;
const sort = sortField ? { field: sortField, order: sortOrder } : null;
this.dataSvc.getPage(page, rows, sort, filters).subscribe({
next: (res: PageResult) => {
this.rows = res.data;
this.total = res.total;
},
error: (err) => {
console.error('Backend error', err);
this.rows = [];
this.total = 0;
// UI can show an error message via a separate template
}
});
}
}
Service Stub
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
interface PageResult {
data: any[];
total: number;
}
@Injectable({ providedIn: 'root' })
export class DataService {
constructor(private http: HttpClient) {}
/**
* @param page 1‑based page number
* @param size number of rows per page
* @param sort optional {field, order}
* @param filters key‑value map matching PrimeNG filterMatchMode
*/
getPage(page: number, size: number, sort: {field:string; order:number} | null,
filters: {[key:string]:any}): Observable {
const params: any = { page, size };
if (sort) { params.sortField = sort.field; params.sortOrder = sort.order; }
Object.keys(filters).forEach(k => { params[`filters_${k}`] = filters[k]; });
return this.http.get('/api/large-data', { params });
}
}
Trust and Data Boundaries
- The table component trusts the backend to return correct
dataandtotalvalues that match the requested slice. - Angular’s change detection runs only when the
rowsreference changes; immutable replacement (as shown) prevents unnecessary re‑renders. - No direct DOM manipulation is performed, so XSS risk is limited to Angular’s built‑in sanitization of interpolated values.
Operational Checks
- Monitor backend latency: ensure each
getPagecall returns within an acceptable threshold (e.g., <200 ms) under load. - Verify that
totalRecordsequals thetotalfield from the service after each successful response. - Debounce rapid scrolling: wrap the handler in a
setTimeoutor use RxJSdebounceTimeif the event fires too frequently. - Log non‑200 HTTP responses and display a user‑friendly error state without throwing.
Failure Modes
- Backend lacks efficient offset‑based queries: each request may scan increasing offsets, causing latency growth. Mitigation: switch to keyset pagination (seek method) and adjust the service accordingly.
- Immutable reference not updated: if the component mutates the existing array instead of assigning a new one, OnPush change detection will not refresh the view, showing stale data.
- FilterMatchMode mismatch: using a mode the backend does not support (e.g., "contains" when the API only supports equality) leads to zero results. Ensure the backend interprets the same match mode or translate on the service layer.
Conditions That Would Change the Design
- Dataset size grows to millions and network bandwidth becomes a bottleneck: consider virtual scrolling combined with lazy loading, or server‑side caching.
- UI requires client‑side aggregations (e.g., summary rows) that need the full dataset: introduce a separate service call for aggregates while keeping the table lazy‑loaded.
- Strict security policies prohibit exposing raw IDs: apply masking or tokenization in the service layer before sending data to the UI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.