PrimeNG Table Pagination: Choosing Client-Side or Server-Side Loading
Decide whether PrimeNG Table should page data in the browser or request each slice from the backend, with version-sensitive implementation notes and validation steps.
05 Jan 2026, 10:39 UTC

The Problem and the Decision
An Angular p-table handles small lists without much thought, but as row count grows the initial payload and DOM weight increase. Browser-side sorting and filtering on a large array can block the main thread, making page turns feel slow. The practical takeaway: use client-side pagination for small, mostly static datasets, and move to server-side lazy loading for large or frequently changing data. Before adopting any code sample, check the PrimeNG major version in package.json; lazy-load event fields and paginator behavior differ between releases.
Constraints That Drive the Choice
- Dataset size: Client-side pagination loads the complete result set into the browser. There is no published PrimeNG row limit; the safe upper bound depends on row complexity and client hardware. Treat a few hundred rows as a practical caution, not a documented cutoff.
- Data volatility: If rows change often, server-side lazy loading returns the current slice on each interaction without reloading the full set.
- Backend control: Server-side mode needs an endpoint that accepts pagination, sort, and filter parameters and returns a page plus
totalRecords. If you cannot change the API, client-side is the only supported option. - Network profile: Client-side makes one request. Server-side makes a request on every page change, sort, or filter update. High-latency connections may feel sluggish without a loading indicator.
Supported Options at a Glance
| Aspect | Client-side ([lazy]='false') | Server-side lazy ([lazy]='true') |
|---|---|---|
| Initial request | One request for all rows | One request for the first page |
| Page, sort, or filter change | No network call; PrimeNG pages locally | New onLazyLoad event and API request |
| Browser memory and DOM | Full dataset held in memory | Only the current page rendered |
| Sorting and filtering | Handled by PrimeNG in the browser | Delegated to the backend |
| Required bindings | [value] only | [value], [totalRecords], (onLazyLoad) |
| Typical fit | Small static lists | Large, dynamic, or security-sensitive data |
Trade-Offs in Practice
Client-side pagination removes round-trips after the first load, so page turns are instant. The cost is the initial JSON payload and the DOM nodes PrimeNG creates for every row. On low-end devices this can cause a visible delay during the first render.
Server-side lazy loading keeps the initial payload small and offloads sorting and filtering to the database. The trade-off is latency on every user action, the need to manage a loading state, and backend work to parse the lazy-load event and build a query. Columns used for sort or filter should be indexed; otherwise each interaction can trigger a full table scan.
Version Assumptions
This guide assumes PrimeNG 15 or later. In that release the lazy-load event carries pagination and sorting metadata, but the exact shape of filters depends on the major version. Older releases may represent filters as an array rather than an object keyed by column name. Open node_modules/primeng/table.d.ts in your project and inspect the TableLazyLoadEvent interface before copying the handler below.
Concrete Implementation: Client-Side
<p-table
[value]='products'
[paginator]='true'
[rows]='10'
[lazy]='false'
>
<ng-template pTemplate='header'>
<tr>
<th pSortableColumn='name'>Name</th>
<th pSortableColumn='category'>Category</th>
</tr>
</ng-template>
<ng-template pTemplate='body' let-product>
<tr>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
</tr>
</ng-template>
</p-table>
Fetch the complete array once, for example in ngOnInit, and assign it to products. PrimeNG handles paging, sorting, and filtering in the browser. No totalRecords binding is required.
Concrete Implementation: Server-Side Lazy Loading
<p-table
[value]='products'
[paginator]='true'
[rows]='10'
[lazy]='true'
[totalRecords]='totalRecords'
[loading]='loading'
(onLazyLoad)='loadProducts($event)'
>
<ng-template pTemplate='header'>
<tr>
<th pSortableColumn='name'>Name</th>
<th pSortableColumn='category'>Category</th>
</tr>
</ng-template>
<ng-template pTemplate='body' let-product>
<tr>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
</tr>
</ng-template>
</p-table>
import { TableLazyLoadEvent } from 'primeng/table';
// ...
loadProducts(event: TableLazyLoadEvent) {
this.loading = true;
const params = {
first: event.first,
rows: event.rows,
sortField: event.sortField,
sortOrder: event.sortOrder,
filters: event.filters
};
this.productService.getPage(params).subscribe({
next: (res) => {
this.products = res.data; // current page only
this.totalRecords = res.total; // required for paginator math
this.loading = false;
},
error: () => {
this.loading = false;
}
});
}
Critical rule: when [lazy]='true', [value] must contain only the current page slice. Supplying the full dataset while lazy mode is on breaks paginator math and can produce duplicate or missing rows.
Validation Steps
- Inspect network traffic: Open browser DevTools, open the Network tab, and reload the page. Client-side pagination shows one data request on load. Server-side lazy loading shows a new request each time you change pages, sort a column, or apply a filter.
- Inspect the lazy-load event: Add
console.log(event)as the first line ofloadProducts. Confirm the object includesfirst,rows,sortField,sortOrder, andfilters. Compare the field names against your installed PrimeNG type definitions. - Check paginator sync: After the first server response, the paginator should show a page count derived from
totalRecords. If it reads '1 of 1' despite many rows,totalRecordsis missing, zero, or does not match the backend total.
Limitations and Gotchas
- Do not mix modes: Setting
[lazy]='true'while passing the full dataset to[value]is unsupported and usually breaks the paginator. - Client-side performance on large arrays: Sorting or filtering thousands of rows in the browser can block the main thread. There is no universal row cap; measure on target devices or switch to server-side.
- Backend indexing: Server-side pagination shifts load to the database. Index any column exposed through
pSortableColumnorpFilter. - Loading state: The
[loading]binding is optional but recommended. Without it, rapid clicks can queue overlapping requests. - Version drift: Re-run these steps after a PrimeNG upgrade because event signatures and template APIs can change between majors.
Quick Decision Flow
- Is the dataset small and mostly static? → Client-side.
- Does the API already support first/rows/sort/filter parameters? → Server-side.
- Can you add those parameters to the API? → Server-side, preferred for large data.
- Otherwise → Client-side with a practical row cap, and plan migration to server-side as data grows.