Implementing PrimeNG p-table Lazy Loading for Large Data Sets
Learn how to enable lazy loading in PrimeNG's p-table so only the visible page of data is fetched from the server, reducing payload and memory usage.
04 Apr 2026, 23:15 UTC

Problem: Loading thousands of rows kills performance
When an Angular application needs to display a table with thousands of records, sending the entire data set to the client at once causes long initial load times, high memory consumption, and sluggish UI interactions. Users typically only need to see a small slice of the data at any moment, so fetching everything is wasteful.
Thesis: PrimeNG's p-table lazy loading lets you request only the visible page
By marking the p-table with the lazy attribute and providing a handler for the onLazy event, you can delegate paging, sorting, and filtering to a backend service. The table then requests just the rows needed for the current view, keeping the payload small and the UI responsive. This behavior is stable across PrimeNG 10 through the 13.x series, so the core logic does not need to change when upgrading within that range.
How lazy loading works in PrimeNG
The p-table component exposes three key bindings for lazy mode:
[lazy]="true"– activates lazy loading.[totalRecords]="totalRecords"– tells the paginator how many rows exist overall.(onLazy)="loadData($event)"– receives a lazy load event containing pagination, sorting, and filtering details.
The handler translates the event into a call to a service method that returns a paginated slice and the total count.
Worked example: Service, component, and template
1. Define a service method signature
The following mock service simulates a paginated backend. Replace the mocked return with a real HTTP call in production. Run this in a standard Angular project; no special permissions are needed.
// src/app/data.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
export interface PageResult<T> {
data: T[];
total: number;
}
@Injectable({ providedIn: 'root' })
export class DataService {
getData(
page: number,
size: number,
sortField: string | null,
sortOrder: number,
filters: Record<string, unknown>
): Observable<PageResult<unknown>> {
return of({
data: this.generateFakeData(page * size, size),
total: 12345
});
}
private generateFakeData(start: number, count: number): unknown[] {
const result = [];
for (let i = 0; i < count; i++) {
const id = start + i;
result.push({ id, name: `Item ${id}`, value: (id * 7) % 100 });
}
return result;
}
}2. Component wiring
// src/app/lazy-table.component.ts
import { Component } from '@angular/core';
import { DataService, PageResult } from './data.service';
@Component({
selector: 'app-lazy-table',
templateUrl: './lazy-table.component.html'
})
export class LazyTableComponent {
totalRecords = 0;
rows: unknown[] = [];
constructor(private dataService: DataService) {}
loadData(event: {
first: number;
rows: number;
sortField?: string;
sortOrder?: number;
filters?: Record<string, unknown>;
}): void {
const page = event.first / event.rows;
const size = event.rows;
const sortField = event.sortField ?? null;
const sortOrder = event.sortOrder ?? 1;
const filters = event.filters ?? {};
this.dataService
.getData(page, size, sortField, sortOrder, filters)
.subscribe({
next: (res: PageResult<unknown>) => {
this.totalRecords = res.total;
this.rows = res.data;
},
error: (err) => console.error('Lazy load failed', err)
});
}
}3. Template
<!-- src/app/lazy-table.component.html -->
<p-table
[value]="rows"
[lazy]="true"
[totalRecords]="totalRecords"
(onLazyLoad)="loadData($event)"
[paginator]="true"
[rows]="10"
[showCurrentPageReport]="true"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="id">ID <p-sortIcon field="id"></p-sortIcon></th>
<th pSortableColumn="name">Name <p-sortIcon field="name"></p-sortIcon></th>
<th pSortableColumn="value">Value <p-sortIcon field="value"></p-sortIcon></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>Note that the event binding is onLazyLoad, the documented output name for the lazy load event in current PrimeNG versions. Check the API documentation for your installed version, as event names can differ between major releases.
Trade-off and limitation
Lazy loading shifts the burden of sorting, filtering, and pagination to the backend. If your API cannot accept the parameters that PrimeNG sends (sortField, sortOrder, filters), the table may show stale or incorrect data when users change sort order or apply filters. Client-side filtering across the full dataset is also no longer possible, since the client only ever holds one page; complex filters must move to the backend or be handled with custom merging logic, which can erode the performance benefits.
Actionable closing
To verify that lazy loading is working:
- Run the application and open the browser's developer tools.
- Navigate to the Network tab and filter for XHR requests to your data endpoint.
- Change pages using the paginator; each page change should trigger exactly one request with parameters matching the page offset, page size, sort field, sort order, and any active filters.
- Confirm that the response contains only the requested slice and that the table updates without re-loading previously fetched pages.
If the requests look correct and the UI reflects the returned data, lazy loading is successfully reducing payload and keeping the interface responsive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.