PrimeNG DataTable: Server-Side Lazy Loading with Virtual Scroll
Combine server-side lazy loading and virtual scrolling in PrimeNG DataTable to keep large datasets responsive without loading every row into the browser.
29 Jul 2025, 20:43 UTC

The problem: a DataTable that slows down as rows accumulate
Your Angular page uses a PrimeNG DataTable to show orders, logs, or customer records. At first it works fine. As the dataset grows past a few hundred rows, the page feels heavy: initial load takes longer, scrolling stutters, and sorting or filtering locks up the browser. The table is likely loading every record into memory and rendering every row in the DOM at once.
Lazy loading means the table asks the backend for only the data it needs for the current page, sort order, or filter. Virtual scrolling means the browser renders only the rows inside the visible viewport, reusing a small set of DOM elements as the user scrolls. DOM nodes are the individual elements—such as table rows—that the browser tracks and paints.
The useful takeaway: you can combine both so the backend handles data volume and the browser handles only what is visible. This is not automatic; you must wire query state to API calls and confirm your PrimeNG version supports the inputs you use.
How the two features divide the work
Server-side lazy loading moves pagination, sorting, and filtering to an API endpoint. The table sends a request containing the requested page, page size, sort field, and active filters. The backend returns one page of results plus a total count. The table never holds the full dataset.
Virtual scrolling limits rendering. Instead of creating a DOM node for every returned row, PrimeNG creates enough rows to fill the screen plus a small buffer. As the user scrolls, it updates the data in those existing nodes rather than appending new ones.
Used together, lazy loading controls how much data travels over the network, and virtual scrolling controls how much of it lives in the DOM at any moment.
Example configuration shape
PrimeNG APIs are version-sensitive. Property names such as lazy, virtualScroll, and event names such as onLazyLoad differ between major releases. Before copying this example, check the documentation for your installed PrimeNG version. You can check the installed version from your project root:
npm list primeng
Run this in the project root. No special permissions are required. The expected output shows the installed PrimeNG version; use that version to confirm the exact template syntax.
A typical template might look like this:
<p-table
[value]="rows"
[lazy]="true"
[virtualScroll]="true"
[virtualScrollItemSize]="48"
[totalRecords]="totalRecords"
[loading]="loading"
(onLazyLoad)="onLazyLoad($event)"
>
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name">Name</th>
<th pSortableColumn="date">Date</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr>
<td>{{ row.name }}</td>
<td>{{ row.date }}</td>
</tr>
</ng-template>
</p-table>
Placeholders such as rows, totalRecords, and loading are component properties you maintain. The virtualScrollItemSize value should match your average row height in pixels; virtual scrolling assumes uniform row heights.
Handling the lazy-load event
In the component class, map the PrimeNG lazy-load event to your data service:
onLazyLoad(event: any): void {
this.loading = true;
this.dataService
.query({
page: event.first / event.rows,
size: event.rows,
sortField: event.sortField,
sortOrder: event.sortOrder,
filters: event.filters
})
.subscribe({
next: (response) => {
this.rows = response.data;
this.totalRecords = response.totalRecords;
this.loading = false;
},
error: () => {
this.loading = false;
}
});
}
The event object shape is not stable across PrimeNG versions. Inspect it in your version or log it to confirm field names such as first, rows, sortField, and filters before relying on them.
Verify it is working in the browser
- Open your app in a browser and open Developer Tools (usually F12).
- Go to the Network tab and filter for XHR or fetch requests.
- Interact with the table: change pages, sort a column, or type a filter. You should see a request to your backend for each interaction, not one large request on initial load.
- Go to the Elements tab and inspect the table rows. With virtual scrolling enabled, the number of row elements should remain roughly constant as you scroll through thousands of records. Without it, the node count grows as more rows render.
- Confirm the pager total matches
totalRecordsso the table knows how many pages exist.
If the table still loads all records up front, or if the DOM node count climbs with every scroll, one of the two features is not wired correctly or your PrimeNG version uses different input names.
Trade-offs and limitations
- Backend complexity: Your API must support pagination, sorting, and filtering. If it returns the full dataset and the table slices it in the browser, you gain little.
- Network latency: Every page change or sort is a round-trip. Slow APIs make the table feel unresponsive regardless of virtual scrolling.
- Uniform row heights: Virtual scrolling calculates scroll position from row height. Wrapped text, images, or custom row templates that vary in height can cause scroll jumps or blank areas.
- Version sensitivity: PrimeNG changes inputs, outputs, and types between major versions. Treat this as a pattern, not a copy-paste API contract.
Practical closing check
Before deploying, open the table with a dataset larger than your viewport, watch the Network tab while paging, and watch the Elements tab while scrolling. If requests are small and scoped to the current page, and the DOM row count stays flat, the combination is working. If either condition fails, check your PrimeNG version documentation first, then review how you map the lazy-load event to your backend.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.