
Data Virtualization in Angular 20+: Smooth 100k‑Row Tables with Angular Material/PrimeNG, Scroll Performance, and Memory Budgets
A practical guide to rendering huge datasets in Angular Material and PrimeNG without jank—covering Signals state, viewport strategy, accessibility, density, and the AngularUX visual language.
Virtualization is the UX gift that keeps on giving: fewer DOM nodes, fewer re-renders, and a table that feels native at 100k rows.Back to all posts
I’ve shipped dashboards that push millions of records—ads analytics at a leading telecom provider, airline kiosk telemetry at a major airline, device fleets at an enterprise IoT hardware company. The pattern that never fails: virtualize aggressively, measure relentlessly, and polish the UX so it feels light even at enterprise scale.
Below is how I implement data virtualization in Angular 20+ with Material/PrimeNG, using Signals and a clean visual language that respects accessibility, density, and brand colors while staying inside a tight performance budget.
When your 100k‑row table scrolls like syrup: the enterprise reality
Seen this movie before
If your Angular table jitters, it’s not your users’ laptops. It’s the DOM count, change detection churn, and row components allocating new objects per frame. As a senior Angular engineer, I’ve fixed this pattern repeatedly: virtualize the viewport, stabilize identity, and move state into Signals.
Charter ads analytics needed 200k-row pivots with smooth scroll and sticky totals.
United kiosks cached offline logs; techs scrolled thousands of events on underpowered hardware.
an enterprise IoT hardware company device fleets streamed WebSocket updates across 50k+ devices with role-based filtering.
Why this matters in 2025 roadmaps
If you need to hire an Angular developer to steady a dashboard before budget season, virtualization is the fastest path to visible wins: scroll feels native, memory drops, and executives see pages respond instantly.
Angular 20+ apps are shipping Signals; tables are a top source of jank.
Recruiters ask for Core Web Vitals; directors ask for measurable ROI.
Hiring a proven Angular consultant reduces risk during Q1 upgrade windows.
Why Angular tables choke on large datasets and how virtualization helps
Root causes of jank
The frame budget is 16.7 ms at 60 fps. If a scroll tick triggers layout + paint + JS over 16.7 ms, you feel it. Virtualization caps visible rows (~20–60) and reuses cells. Combined with Signals, you cut work per frame dramatically.
Rendering thousands of DOM nodes at once.
Change detection across deep trees (even OnPush can thrash if identity isn’t stable).
GC pressure from new view models per scroll tick.
Expensive cell templates (pipes, date/number formatting, nested components).
What good looks like
I target these budgets on dashboards at AngularUX. The difference is night and day on executive laptops and kiosk hardware.
<16 ms scripting during fast scroll; <3 ms style/layout; <3 ms paint.
Heap stable under 150 MB on 100k+ rows.
No focus loss, sticky header stays pinned, keyboard paging works.
Implementing data virtualization in Angular Material and PrimeNG tables (Angular 20+)
import { signal, computed, effect, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
interface Row { id: string; ts: number; campaign: string; impressions: number; cost: number; }
export class TableStore {
private http = inject(HttpClient);
// Source data is chunked via API; we hold only a window + cache
private pageSize = 1000;
private cache = new Map<number, Row[]>(); // page -> rows
density = signal<'compact'|'cozy'|'comfortable'>('compact');
rowHeight = computed(() => this.density() === 'compact' ? 32 : this.density() === 'cozy' ? 40 : 48);
total = signal(100_000);
viewportOffset = signal(0); // index of first visible row
viewportCount = signal(50);
// Compute which pages we need for current viewport + small buffer
neededPages = computed(() => {
const start = Math.max(0, Math.floor(this.viewportOffset() / this.pageSize) - 1);
const end = Math.floor((this.viewportOffset() + this.viewportCount() + 500) / this.pageSize) + 1;
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
});
visible = computed<Row[]>(() => {
const start = this.viewportOffset();
const end = Math.min(this.total(), start + this.viewportCount() + 200); // small buffer
const rows: Row[] = [];
for (let i = Math.floor(start / this.pageSize); i <= Math.floor(end / this.pageSize); i++) {
const page = this.cache.get(i) ?? [];
rows.push(...page);
}
// slice within the concatenated pages
const offsetInConcat = start % this.pageSize;
return rows.slice(offsetInConcat, offsetInConcat + (end - start));
});
constructor() {
effect(() => {
for (const p of this.neededPages()) {
if (!this.cache.has(p)) this.loadPage(p);
}
});
}
private loadPage(p: number) {
this.http.get<Row[]>(`/api/rows?page=${p}&size=${this.pageSize}`, { observe: 'body' })
.subscribe(rows => this.cache.set(p, rows));
}
}<!-- Angular Material approach -->
<cdk-virtual-scroll-viewport [itemSize]="store.rowHeight()" class="viewport">
<table mat-table [dataSource]="store.visible()" [trackBy]="trackById">
<!-- columns here -->
</table>
</cdk-virtual-scroll-viewport>
<!-- PrimeNG approach -->
<p-table
[value]="store.visible()"
[virtualScroll]="true"
[scrollHeight]="'70vh'"
[virtualScrollItemSize]="store.rowHeight()"
[tableStyle]="{ 'font-size.px': densityFontPx(store.density()) }"
[trackBy]="trackById">
<!-- p-column templates here -->
</p-table>// component.ts
trackById = (_: number, row: { id: string }) => row.id;
onScrolledIndexChange(index: number) {
// hook this to cdk-virtual-scroll-viewport (scrolledIndexChange) or p-table virtual scroll event
this.store.viewportOffset.set(index);
}1) Pick your viewport strategy
Material gives you surgical control and works great with Signals. PrimeNG’s p-table is ergonomic for enterprise CRUD. I use whichever aligns with your design system and component stack.
Angular Material: cdk-virtual-scroll-viewport + *cdkVirtualFor.
PrimeNG: p-table with [virtualScroll] and p-virtualScroller for grids.
Avoid mixing two virtualizers for the same rows.
2) Model viewport + selection state with Signals
Signals isolate state updates so a scroll doesn’t cause global change detection. Below is a trimmed SignalStore used on a Charter-style analytics table.
Use a SignalStore for source data, visible slice, selection, and density.
Expose computed signals for currentRange, total, and columns.
SignalStore example
Accessibility, typography, density, and colors in virtualized tables
:root {
--ux-font-12: 12px; --ux-font-14: 14px; --ux-font-16: 16px; --ux-font-20: 20px;
--ux-row-compact: 32px; --ux-row-cozy: 40px; --ux-row-comfy: 48px;
--ux-surface: #0f1115; // AngularUX dark surface
--ux-muted: #1a1d24; // table stripes
--ux-text: #e6ebf2; // high contrast text
--ux-accent: #4cc2ff; // selection/hover
--ux-focus: #ffd166; // focus ring
}
.table--compact { --row-h: var(--ux-row-compact); font-size: var(--ux-font-12); }
.table--cozy { --row-h: var(--ux-row-cozy); font-size: var(--ux-font-14); }
.table--comfy { --row-h: var(--ux-row-comfy); font-size: var(--ux-font-16); }
.table {
background: var(--ux-surface);
color: var(--ux-text);
tr { height: var(--row-h); }
tr:nth-child(even) { background: var(--ux-muted); }
tr[aria-selected="true"] { outline: 2px solid var(--ux-accent); }
td:focus { outline: 2px solid var(--ux-focus); outline-offset: -2px; }
}<!-- a11y assist for range changes -->
<div class="sr-only" aria-live="polite">Showing rows {{start}} to {{end}} of {{store.total()}}</div>A11y patterns that survive virtualization
Virtualization should be invisible to assistive tech. Ensure keyboard paging (PageUp/PageDown) updates the viewportOffset, and preserve selection state in a SignalStore keyed by row.id.
Add role="table"/"row"/"cell"; keep sticky header aria-hidden=false.
Retain focus on cells during scroll; move focus only on user intent.
Announce virtual range changes to screen readers with aria-live="polite" (offscreen).
AngularUX tokens: typography, density, color
Consistent tokens stabilize virtualization math and improve readability. Here’s a minimal token set and usage.
Lock row heights to density tokens (32/40/48 px).
Typography scale: 12/14/16/20; keep data rows at 12–14px, headers at 14–16px.
AngularUX palette: --ux-surface, --ux-muted, --ux-accent for selection/hover.
SCSS tokens
Memory optimization: pooling and cell costs
Pool row view models
GC churn kills scroll. If you can, reuse row VM objects and update fields in place while keeping stable ids for trackBy.
Reuse a fixed array for visible rows; mutate in place.
Avoid JSON.parse(JSON.stringify(...)) clones; use immutable updates only when needed.
Cheap cells win
At a broadcast media network, we cut 40% scripting time by preformatting currency and using a lightweight cell component with OnPush and signals for highlight state only.
Preformat numbers/intl on the server or at page fetch.
Prefer pure pipes or precomputed strings; avoid new Date() in templates.
Avoid nested components inside cells when possible (or make them OnPush + signals).
Measure heap + frames
Keep a baseline CSV of runs in CI so regressions are obvious to non-engineers. Directors understand trendlines.
Chrome DevTools Performance + Memory tabs.
Firebase Performance for page load and custom traces.
Target <150 MB heap for 100k rows; <16 ms scripting during fling.
Real-time updates with WebSocket + Signals without render storms
type Event =
| { type: 'row-update'; id: string; patch: Partial<Row> }
| { type: 'bulk-replace-page'; page: number; rows: Row[] }
| { type: 'meta'; total: number };
const ws$ = new WebSocket('wss://api.example.com/stream');
// pseudo: convert to Rx stream then to Signal
// update only cached pages; if a row is visible, mutate in place to retain identity
function apply(e: Event) {
if (e.type === 'row-update') {
// find page and row; mutate in place
}
}Typed events + selective updates
This is how we kept an enterprise IoT hardware company’s device grid calm while ingesting thousands of status pings per minute.
Use a discriminated union for event types.
Map events to the cached page only if the row is in view.
Debounce coalesced updates to 60 Hz max.
Event schema
CI guardrails with Nx and Firebase Performance
# project.json (Nx)
"targets": {
"perf": {
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"lhci autorun --collect.url=http://localhost:4200/table --assert.assertions.'performance'>=0.9",
"node scripts/heap-check.js --route=/table --maxHeapMB=150"
]
}
}
}Performance budgets in CI
We used this on gitPlumbers to protect 99.98% uptime while modernizing views, and the same gates keep tables fast as code evolves.
Run Lighthouse CI against a virtualized table route.
Push custom traces to Firebase Performance in E2E runs.
Fail PRs if rendering budget or heap delta exceeds threshold.
Nx target example
Enterprise examples—and when to hire an Angular developer for virtualization rescue
Case snapshots
These wins are repeatable with disciplined state, identity, and viewport math.
Charter ads analytics: 100k-row pivots, PrimeNG virtualScroll, preformatted currency; 2.1x faster scroll.
United kiosks: offline logs with CDK Virtual Scroll; Docker-simulated hardware; stable on low-end CPUs.
a global entertainment company employee tracking: role-based tables with sticky summaries; memory held under 120 MB.
When to bring in a consultant
If these symptoms sound familiar, it’s time to hire an Angular expert who’s done it at scale. I can assess within a week and ship fixes in 2–4 weeks depending on complexity.
Jank during fast scroll or long list rendering > 50 ms.
Heap spikes or tab crashes on large datasets.
Screen reader usability breaks during virtualization.
Concise takeaways
- Virtualize rows/columns and lock rowHeight to density tokens for stable math.
- Use Signals/SignalStore to isolate viewport and selection; trackBy stable ids.
- Pool row VMs and keep cells cheap; preformat data.
- Validate a11y, sticky headers, keyboard paging, and focus retention.
- Guard budgets with Nx CI, Lighthouse, and Firebase Performance traces.
- Measure: <16 ms scripting during scroll; heap <150 MB at 100k rows.
How an Angular consultant can help: next steps
If you need a remote Angular developer to steady a role-based dashboard, I’m available for select projects. Let’s review your Angular 20+ roadmap and ship a fast, accessible table that your PM can demo without apologizing for scroll jitter.
What we’ll do in week one
I’ve done this for a broadcast media network VPS scheduling, an insurance technology company telematics dashboards, and a cloud accounting platform accounting. The pattern scales across industries.
Instrument your heaviest table route with DevTools + Firebase.
Swap in CDK or PrimeNG virtualization and Signals state.
Set CI gates in Nx and deliver a before/after report your execs will love.
Key takeaways
- Virtualize rows and columns; never render more than the viewport + buffer.
- Use Signals/SignalStore to isolate viewport state and cut re-renders by 70%+.
- Lock rowHeight and density tokens to stabilize virtualization math.
- Track by stable IDs and pool row view models to reduce GC churn.
- Guard performance in CI with Lighthouse/Cypress thresholds and Firebase Performance traces.
- Apply accessibility: aria roles, focus retention, sticky headers, and offscreen announcement patterns.
- Measure memory: aim <150 MB heap for 100k+ rows and <16.7 ms frame budget.
Implementation checklist
- Define rowHeight, buffer size, and density tokens in your theme.
- Pick a viewport strategy: CDK Virtual Scroll or PrimeNG virtualScroll.
- Create a SignalStore for pagination/viewport state and selection.
- Implement trackBy with stable IDs and object pooling.
- Instrument performance (DevTools, Firebase Performance) and set CI thresholds via Nx.
- Add a11y: aria roles, sticky headers, keyboard paging, focus retention.
- Validate typography and colors against AngularUX tokens; test contrast and hover states.
- Load detail panes on demand; avoid row expansion in the virtual list DOM.
Questions we hear from teams
- How long does data virtualization take to implement in an existing Angular app?
- Most teams see results in 1–2 weeks for a single heavy table: day 1–2 assessment, day 3–5 implementation with Signals and virtual scroll, and a second week for a11y polish and CI guardrails. Larger refactors or multiple tables add 1–3 weeks.
- Do we need Angular Material or PrimeNG for virtualization?
- No. Angular CDK Virtual Scroll works with plain tables or Material; PrimeNG has built-in virtualScroll. I choose based on your design system and team skills—both deliver smooth 100k-row scroll when configured correctly.
- Will virtualization break accessibility for screen readers?
- It shouldn’t. With correct roles, sticky headers, focus retention, and aria-live announcements for range changes, virtualized tables are accessible. We validate with keyboard testing and automated checks in CI.
- What does a typical Angular engagement look like and cost?
- Discovery call in 48 hours, assessment within a week, implementation 2–4 weeks. Pricing depends on scope and risk. I offer fixed-fee deliverables for a single table or time-and-materials for broader dashboard refactors.
- Can you handle real-time updates without re-render storms?
- Yes. I use typed event schemas, Signals for selective updates, and coalescing at 60 Hz. This kept an enterprise IoT hardware company’s device grids stable under thousands of updates per minute.
Ready to level up your Angular experience?
Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.
NG Wave
Angular Component Library
A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.
Explore Components