← Case studies

Case Study

List Management Standards

A set of performance budgets, state rules, and interaction patterns for enterprise list views that remain usable from small datasets through 10K-row workloads — without per-feature drift or inconsistent UX decisions.

Performance Angular CDK NgRx Signals LCP / INP / CLS Virtualization

The Problem

List pages are deceptively complex. Sorting, filtering, bulk selection, empty states, permissions, pagination, virtualization, and responsive behaviour all create opportunities for implementation drift across teams. Without shared standards, each feature area was making independent decisions — some loading all rows into the DOM, some using flat 10 s polling intervals, some persisting filter state where it should reset, some not.

The goal was a repeatable model: one set of performance budgets, state storage rules, and interaction patterns that any team could reference when building or reviewing a list view, without needing a case-by-case design review for every feature.

Measured Performance Baselines

Before writing rules, I measured the existing list rendering performance using the platform’s question panel as a representative component. The results made the scale thresholds concrete rather than arbitrary:

Row count LCP (s) ≤ 2.5 good INP (ms) ≤ 200 good CLS ≤ 0.1 good
100 1.62 207 0.26
1,000 2.86 625 0.03
5,000 5.83 3,085 0.23
10,000 9.12 5,254 0.03

Interactivity (INP) degrades before load time (LCP) does. At 1,000 rows the page loads in a borderline 2.86 s but interactions are already slow at 625 ms. This gave us a clear architectural breakpoint: frontend rendering is viable up to roughly 1,000 rows with optimisations; beyond that, the work must shift to the backend.

Architecture decision

For datasets expected to exceed ~1,000 rows, move filtering, sorting, and grouping to the backend and return paginated or server-filtered results. For smaller datasets, frontend computation within a 100 – 150 ms interaction budget is acceptable with debounce, trackBy, and untracked() applied.

Virtualization

When introducing virtual scrolling, aim for consistent row heights. Angular CDK virtual scrolling works reliably with fixed heights; the auto-size strategy for variable-height rows is experimental and can cause blank regions or scroll bar stutters. Custom height estimation is possible but significantly increases maintenance cost against future layout changes.

The practical rule: if rows can be made a consistent height without sacrificing content, prefer fixed-height virtual scrolling. If content varies significantly (e.g. multi-line descriptions, expandable details), invest in the auto-size approach only if the dataset is expected to regularly exceed a few hundred rows.

Loading States

Skeleton animations should be used on any design-system component that provides one. If a component doesn’t have a skeleton, a section- or panel-level loading indicator is required. Dependent interactions (bulk actions, filters, form controls) must be blocked until data is ready to prevent partial or invalid user actions.

State Management: Where Things Live

One of the most common inconsistencies was where UI state was persisted. The standard defines three tiers:

Storage tier What belongs here Clears on
Component / signal Selections, text search input, scroll position Route change
Session storage Filters, sorting, group expansion state Tab / browser close
Local storage Column chooser configuration, heavy layout preferences Explicit user reset
Database Saved views, named filters, anything the user explicitly saves User action only

The principle: if a setting is lightweight and the user wouldn’t be bothered by resetting it on their next visit, use session storage. If reproducing it is tedious (e.g. configuring a 12-column grid layout), persist it locally. If the user explicitly named and saved it, it belongs in the database.

Filtering, Sorting, and Grouping

  • Filters, sort state, and group expansion should persist across in-page interactions and drill-downs. Route changes clear scroll position and selections only — not filter state.
  • Every filter or sort must have a one-click path back to default state. Users should never have to hunt to reset a view.
  • Filtering and bulk actions work in tandem: the filtered dataset is the subset the bulk action operates on.
  • Fields are sortable by default unless explicitly excluded.
  • Data interactivity target: 200 ms end-to-end. With Angular change detection (20 – 50 ms) and virtual scroll recalculation (10 ms) in the budget, filter computation has roughly 100 – 150 ms to complete.

Grouped datasets

When filtering a grouped dataset, retain parent/lineage nodes for structural context but visually de-emphasise them and exclude them from result counts. When sorting a grouped dataset, hierarchy takes priority — nodes sort among their siblings, not globally across the flat list.

Empty and Error States

Three distinct states — each requires its own treatment:

  • No results from filter. “No results match your criteria” with a clear path to reset the filter.
  • No data exists. Distinct empty-state component with context-specific messaging and a primary action (e.g. “Create your first report”).
  • Load error. Toast notification for the error, and an inline component state explaining what failed and what to do next.

These were identified as a gap: the platform lacked reusable empty-state and error components with standardised messaging, leading to ad-hoc implementations across feature areas.

Selection and Bulk Actions

For selections that feed into form submission or need validation and dirty tracking, use Angular Reactive Forms. For transient UI selection state that just drives an action, Angular Signals or Angular CDK’s SelectionModel are appropriate.

When a user changes filter criteria while holding selections, the default is to persist those selections (users often want to select across multiple filtered subsets). This requires surfacing a clear count or summary of current selections so users know their prior choices are still in effect.

If an operation has performance limits, align the maximum selection count with the pagination size, warn the user when they approach the threshold, and break large requests into batched async queues rather than blocking a single large call.

Impact

The standard gave teams a shared reference for list-view decisions, reducing the number of inconsistencies caught during PR review and design QA. It also formed the basis for Cursor AI rules — committed alongside the documentation so the tooling could enforce the patterns automatically during implementation.

Note

This document was a proposal and guideline, not a completed implementation audit. Several outstanding gaps were identified — including skeleton loader coverage, reusable empty-state components, and virtual scrolling rollout across all list views — and noted as follow-up work.