Implementing Server‑Side Lazy Loading in PrimeNG p‑table for Large Datasets
Learn how to enable lazy loading in PrimeNG’s p‑table so only the visible page of data is fetched from the backend, reducing initial load time and memory usage.
06 Feb 2026, 03:53 UTC

Problem: Slow Render When Showing Thousands of Rows
When a PrimeNG p-table is bound to a large array (e.g., thousands of car records), the component loads the entire dataset into the client‑side data array on initialization. This causes a noticeable delay during the first render, high memory consumption, and sluggish interactions as Angular change detection runs over the full list.
Thesis: Enable Lazy Loading to Fetch Only the Visible Page
By setting [lazy]="true" and providing an onLazyLoad handler, the table asks the component for data only when it needs to display a new page, sort column, or apply a filter. The handler calls a backend service that returns the requested slice and the total record count, letting the paginator work correctly.
Worked Example: Cars Table with Server‑Side Pagination
Template
<p-table #dt [value]="cars" [lazy]="true" (onLazyLoad)="loadCars($event)"
[totalRecords]="totalRecords" [rows]="10" paginator="true"
sortMode="multiple">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="vin">VIN<p-sortIcon field="vin"></p-sortIcon></th>
<th pSortableColumn="year">Year<p-sortIcon field="year"></p-sortIcon></th>
<th pSortableColumn="brand">Brand<p-sortIcon field="brand"></p-sortIcon></th>
<th pSortableColumn="color">Color<p-sortIcon field="color"></p-sortIcon></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-car>
<tr>
<td>{{car.vin}}</td>
<td>{{car.year}}</td>
<td>{{car.brand}}</td>
<td>{{car.color}}</td>
</tr>
</ng-template>
</p-table>
Component TypeScript
import { Component, OnInit } from '@angular/core';
import { CarService } from './car.service';
import { LazyLoadEvent } from 'primeng/api';
interface Car {
vin: string;
year: number;
brand: string;
color: string;
}
@Component({
selector: 'app-car-table',
templateUrl: './car-table.component.html'
})
export class CarTableComponent implements OnInit {
cars: Car[] = [];
totalRecords = 0;
constructor(private carService: CarService) {}
ngOnInit(): void {
// Initial load – triggers lazy load for first page
}
loadCars(event: LazyLoadEvent): void {
const request = {
first: event.first ?? 0,
rows: event.rows ?? 10,
sortField: event.sortField ?? '',
sortOrder: event.sortOrder ?? 1,
filters: this.convertFilters(event.filters ?? {})
};
this.carService.getCars(request).subscribe({
next: (response) => {
this.cars = response.data; // array of cars for the page
this.totalRecords = response.total; // total count for paginator
},
error: (err) => {
console.error('Failed to load cars', err);
// Optionally show a message to the user
this.cars = [];
this.totalRecords = 0;
}
});
}
private convertFilters(filters: { [key: string]: any }): { [key: string]: any } {
// PrimeNG provides filter objects with matchMode and value
const converted: { [key: string]: any } = {};
for (const key in filters) {
const f = filters[key];
converted[key] = {
value: f.value,
matchMode: f.matchMode
};
}
return converted;
}
}
Service Stub (for illustration)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface PageResponse {
data: Car[];
total: number;
}
@Injectable({ providedIn: 'root' })
export class CarService {
private apiUrl = '/api/cars';
constructor(private http: HttpClient) {}
getCars(params: {
first: number;
rows: number;
sortField?: string;
sortOrder?: number;
filters?: { [key: string]: { value: any; matchMode: string } };
}): Observable {
return this.http.get(this.apiUrl, { params: params as any });
}
}
Trade‑Offs and Limitations
- Backend dependency: The table now shows empty rows while waiting for the network response. A loading indicator (
p-blockUIor a custom spinner) should be displayed during the request to avoid a confusing blank state. - Error handling: If the service fails, the component must clear the data array and inform the user; otherwise stale data may remain visible.
- Total record count: The backend must return an accurate total; otherwise the paginator will show incorrect page numbers or hide the last page.
- Subscription management: The
getCarscall returns an observable that completes after a single emission, so explicit unsubscription isn’t required. However, if you switch to a long‑lived stream (e.g., usingswitchMapwith a subject), remember to unsubscribe inngOnDestroy.
Verification Steps
- Run the application with a backend that artificially delays responses (e.g.,
setTimeoutin a mock service). - Open the browser’s network tab and confirm that each page change, sort, or filter triggers a request containing
first,rows,sortField,sortOrder, and the relevant filter parameters. - Observe that only the requested slice of data appears in the table and that a loading indicator is visible while the request is pending.
- Check that the paginator’s “total records” matches the count returned by the backend and that navigating to the last page works correctly.
Actionable Closing
If your PrimeNG p-table is struggling with large datasets, enable lazy loading as shown above. Start by adding [lazy]="true" and implementing a simple load method that calls your existing API with pagination, sort, and filter arguments. Remember to handle loading states and errors, and verify the backend returns the total record count. This change will reduce initial render time, lower memory usage, and keep the UI responsive even with hundreds of thousands of rows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.