Combining PrimeNG Lazy Loading with Virtual Scroll for Large Data Tables
Learn how to pair PrimeNG’s lazy‑loading p‑table with virtual scroll to keep UI responsive, reduce DOM size, and enable server‑side paging on massive datasets.
27 Jul 2026, 08:44 UTC

Why Combine Lazy Loading and Virtual Scroll?
When a table shows thousands of rows, two common performance killers surface:
- Angular renders every row into the DOM, exhausting memory and slowing the browser.
- The server must return a large payload, increasing latency and load on the backend.
PrimeNG’s p-table offers two orthogonal mitigations:
- Lazy loading fetches only the requested page from the server.
- Virtual scroll keeps only the visible rows in the DOM, regardless of how many pages exist.
When you enable both, the client asks the server for a slice of data on demand, and the UI keeps the DOM minimal. The result is a responsive table that can display millions of rows without choking the browser.
Step 1 – Install PrimeNG and Angular CDK
Virtual scroll relies on Angular CDK’s cdk-virtual-scroll-viewport. Install the required packages if you haven’t yet:
npm install primeng primeicons @angular/cdk
Make sure your Angular version is 13 or newer, as PrimeNG 13+ aligns with Angular 13+. Add the PrimeNG modules to your app module:
import { TableModule } from 'primeng/table';
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [
TableModule,
ScrollingModule,
// other imports
]
})
export class AppModule {}
Step 2 – Configure the Table for Lazy Loading
Set [lazy]="true" on p-table and bind to the onLazyLoad event. The event receives a LazyLoadEvent object that contains first (offset) and rows (page size). Return a Promise that resolves to an object with data and totalRecords:
<p-table
[value]="data"
[lazy]="true"
[virtualScroll]="true"
[virtualScrollItemSize]="40"
[rows]="50"
[totalRecords]="totalRecords"
(onLazyLoad)="loadLazy($event)"
scrollHeight="400px"
>
<ng-template pTemplate="header">
<tr>
<th>ID</th>
<th>Name</th>
<th>Date</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr>
<td>{{row.id}}</td>
<td>{{row.name}}</td>
<td>{{row.date | date:'short'}}</td>
</tr>
</ng-template>
</p-table>
Notice the [virtualScrollItemSize]="40" attribute. It tells CDK that each row is 40 px tall. Consistency is key; if rows vary in height, the scroll calculations will become inaccurate.
Component Logic
Implement loadLazy to call a service that fetches a page from the server. The service should return a Promise or Observable that resolves to an object with data and totalRecords:
export class DemoComponent {
data: any[] = [];
totalRecords = 0;
loadLazy(event: LazyLoadEvent): void {
const page = event.first! / event.rows!;
const pageSize = event.rows!;
this.myService.fetchPage(page, pageSize).then(result => {
this.data = result.data;
this.totalRecords = result.totalRecords;
});
}
}
@Injectable()
export class MyService {
fetchPage(page: number, size: number): Promise<{data: any[], totalRecords: number}> {
const params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<{data: any[], totalRecords: number}>('/api/items', { params })
.toPromise();
}
}
Replace /api/items with your actual endpoint. The response must include a totalRecords field so PrimeNG can render the paginator correctly.
Step 3 – Enable Virtual Scroll
Virtual scroll is activated by setting [virtualScroll]="true" and optionally [virtualScrollItemSize]="40". The scrollHeight defines the viewport height; PrimeNG will automatically wrap the table body inside a CDK virtual scroll viewport.
When both lazy loading and virtual scroll are active, the table will request a new page from the server only when the user scrolls near the end of the current data. The viewport keeps the DOM size roughly equal to the number of visible rows plus a small buffer, regardless of the total dataset size.
What Happens Under the Hood?
- The user scrolls. CDK calculates the new visible index range.
- If the range extends beyond the currently loaded data, PrimeNG fires
onLazyLoadwith the neededfirstandrows. - The server returns the slice and the total count.
- PrimeNG updates
dataandtotalRecords, and the viewport renders only the new rows.
Trade‑offs and Limitations
- Consistent Row Height: Virtual scroll assumes a fixed height. If you use dynamic templates that change height (e.g., expandable rows), the scroll position may jitter. Either enforce a fixed height or disable virtual scroll.
- Latency Visibility: A slow server can cause empty rows to appear until data arrives. Adding a loading overlay or skeleton rows improves the perceived performance.
- Pagination UI: Because data is loaded lazily, the paginator will show all pages based on
totalRecords, but only one page is rendered at a time. Users may expect the paginator to load the selected page on click; ensure your server supports that behavior. - Testing Complexity: Unit tests must mock the service and ensure the component correctly updates
dataandtotalRecords. Verify thatonLazyLoadis called with the expected parameters when scrolling.
Practical Verification Checklist
- Run
ng serveand open the table. - Open DevTools → Network. Scroll quickly and confirm that only a handful of
GET /api/itemsrequests appear. - Switch
[virtualScroll]="false"temporarily. Scroll and observe the DOM node count (e.g.,document.querySelectorAll('tr').length). It should grow linearly with the dataset size, confirming that virtual scroll keeps the DOM small. - Introduce a deliberate delay in the service (e.g.,
setTimeout1 s). Observe the UI and add ap-progressSpinneroverlay while data is loading. - Test with a row template that adds a
divonly when a property is true. Verify that the scroll position remains stable or decide to disable virtual scroll.
Actionable Takeaway
For tables that may show tens or hundreds of thousands of rows, enable lazy loading and virtualScroll on p-table. Keep row height consistent and provide a responsive loading UI. This combination drastically reduces DOM size, keeps the browser responsive, and scales the backend to fetch only the needed data slices.
Start by adding the attributes shown above, adjust virtualScrollItemSize to match your row height, and verify through the network and DOM checks. Once confirmed, you’ll have a high‑performance table that can grow without slowing down your users’ browsers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.