Lazy-Loading PrimeNG DataTable with Remote Pagination for Large Datasets
Learn how to enable lazy loading in PrimeNG’s DataTable with remote pagination so large datasets stay fast and memory‑efficient.
11 Apr 2026, 01:34 UTC

Problem: Loading thousands of rows kills UI responsiveness
When a PrimeNG p-table is bound to a large array, the browser must render every row at once. Scrolling becomes sluggish, memory spikes, and the user sees a blank screen while the data loads. For datasets that can grow beyond a few thousand records, this approach is untenable.
Thesis: Enable lazy loading with remote pagination so the table only requests and displays the slice of data needed for the current page.
By setting [lazy]="true" and handling the paginator’s onPageChange event, the DataTable asks the backend for a specific page. The backend returns only the requested rows plus a total count, allowing the paginator to compute the correct number of pages while keeping the frontend lightweight.
How lazy loading works in PrimeNG
The DataTable component exposes two key inputs for lazy mode:
[value]– expects the array of rows for the current page.[lazy]– when true, the table stops trying to page locally and instead relies on external events.
The attached paginator emits onPageChange with an object containing:
first– zero‑based index of the first row on the requested page.rows– number of rows per page.
Your service method receives these values, calculates the appropriate offset and limit, queries the database, and returns an object like:
{
data: [...], // array of rows for the page
totalRecords: 12345
}
The DataTable then updates its internal value with data and sets the paginator’s totalRecords to the supplied count.
Worked example: Angular component and service
Assume a REST endpoint /api/items that accepts offset and limit query parameters and returns JSON in the shape above.
Service (items.service.ts)
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
interface PageResponse {
data: any[];
totalRecords: number;
}
@Injectable({ providedIn: 'root' })
export class ItemsService {
private apiUrl = '/api/items';
constructor(private http: HttpClient) {}
getPage(offset: number, limit: number): Observable {
return this.http.get(this.apiUrl, {
params: { offset: offset.toString(), limit: limit.toString() }
});
}
}
Component template (items.component.html)
<p-table #dt [value]=items [lazy]=true (onLazyLoad)=loadItems($event) [paginator]=true>
<ng-template pTemplate="header">
<tr>
<th>ID</th>
<th>Name</th>
<th>Created</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.created | date:'short'}}</td>
</tr>
</ng-template>
</p-table>
Component class (items.component.ts)
import { Component } from '@angular/core';
import { LazyLoadEvent } from 'primeng/api';
import { ItemsService, PageResponse } from './items.service';
@Component({
selector: 'app-items',
templateUrl: './items.component.html'
})
export class ItemsComponent {
items: any[] = [];
constructor(private itemsService: ItemsService) {}
loadItems(event: LazyLoadEvent): void {
const first = event.first ?? 0;
const rows = event.rows ?? 10;
this.itemsService.getPage(first, rows).subscribe({
next: (resp: PageResponse) => {
this.items = resp.data;
// PrimeNG paginator reads totalRecords from the component implicitly
// via the paginator binding; we expose it through a getter if needed.
(this as any).totalRecords = resp.totalRecords;
},
error: (err) => console.error('Failed to load page', err)
});
}
}
Place the component in a module that imports TableModule and PaginatorModule from PrimeNG.
Trade‑off: Backend must support efficient paging
The frontend gains low memory usage and fast initial render, but the backend must be able to return a slice of data quickly and also provide an accurate total record count. If the database lacks proper indexing on the columns used for ordering or filtering, each page request could become slow, negating the UI benefit. Additionally, any client‑side sorting or filtering must be sent to the server; otherwise the table will display mismatched data.
Actionable closing: Verify and iterate
- Run the application and open the browser’s Network tab.
- Navigate to the page containing the
p-table. Observe that the first request includesoffset=0&limit=10(or your chosen page size). - Click the paginator’s next button; confirm the request updates
offsetaccordingly and the table displays the new slice. - Check that the paginator shows the correct total pages based on the
totalRecordsreturned by the service. - If the UI lags, add server‑side indexes on the columns used for ordering and ensure the query uses limit/offset efficiently.
When the network requests line up with the paginator’s first and rows values and the table renders only the visible rows, lazy loading is working as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.