
Signals + Design Tokens Refresh: Cut Render Counts and Lift Lighthouse Scores in Angular 20 (Case Study)
How a Signals + tokenized theme rollout stabilized a jittery analytics dashboard and turned a 68 Lighthouse score into a 92—without freezing feature work.
“Signals give you honest state; tokens give the browser stable geometry. Put them together and the jitter disappears.”Back to all posts
I’ve seen the same movie in aviation, media, and telecom: a beautiful Angular dashboard that jitters under load and leaves performance scores stuck in the yellow. The fix isn’t a rewrite. It’s tightening state with Signals and giving the UI a stable contract with design tokens.
This case study covers a refresh I led for a leading telecom provider’s advertising analytics dashboard. We cut re-renders by 86% on the busiest view and lifted Lighthouse performance from 68 to 92—while shipping new features weekly.
The Night the Dashboard Stopped Jittering: A Telecom Analytics Dashboard
Challenge
The dashboard ingested real-time ad delivery metrics. During peak hours, a single filter change caused cascading change detection and reflows. We were on Angular 18 at the start; we planned the move to Angular 20, introduced Signals, and refreshed the theme using design tokens without pausing feature delivery.
Constraints: production data volume, PrimeNG components, multitenant RBAC, and an Nx monorepo. Success required measurable UX wins and zero downtime.
Filter interactions triggered hundreds of renders across the graph canvas and tables.
Dynamic theme utilities toggled classes and inline styles, causing layout thrash.
Performance triage was anecdotal; leadership saw a 68 Lighthouse score and stalled Core Web Vitals.
Intervention
We targeted the heaviest route first: a table + graph combo with three levels of filters. State moved to a SignalStore that computed derived views and throttled async joins. The theme shifted from utility classes to stable CSS variables so components rendered once and let the browser handle paint updates.
Migrate hot-path state to Signals + SignalStore.
Replace style churn with a tokenized theme (CSS variables) wired to Signals.
Add DevTools + Lighthouse CI guardrails; ship behind feature flags.
Measurable Result
The jitter disappeared. Executives got a simple view of before/after metrics in GA4 and a weekly Lighthouse CI report posted to Slack.
Render count on filter change: 420 → 58 (-86%).
Lighthouse Performance (mid-tier laptop): 68 → 92.
Core Web Vitals: INP 180ms → 95ms; LCP 2.7s → 1.8s; CLS 0.08 → 0.02.
Why Angular Dashboards Waste Renders Without Signals and Tokens
Hidden Multipliers
In legacy code I often see a web of BehaviorSubjects and EventEmitters. Each filter writes to three subjects, each feeding multiple components. Add a theme toggle that flips classes and inline styles and you’ve created a re-render multiplier. Signals with fine-grained reactivity + tokenized CSS variables decouple the JS updates from reflow-heavy DOM churn.
BehaviorSubject sprawl triggers cascading updates.
Mutable state in services prevents view-level memoization.
Theme toggles mutate classes, causing style recalculation and layout shifts.
What Signals Change
Signals reduce guesswork. With Angular DevTools we can see exactly which computed dependencies fire on an interaction and prune the tree. SignalStore gives us a typed home for derived state—think role-filtered datasets, pagination windows, and chart series transforms.
Fine-grained dependency tracking—components re-render only when their inputs change.
Computed() and effect() create predictable, observable state transitions.
SignalStore coalesces writes and centralizes derivations.
Why Tokens Matter
Design tokens are the mechanical sympathy our dashboards lacked. Once spacing and density are tokens—not utility classes—LCP improves because above-the-fold layout isn’t recalculated by JS on interaction.
Tokens give the UI a stable contract—density, color, spacing, radius become platform variables.
CSS variables are updated once; components inherit without re-instantiation.
PrimeNG + Material both map cleanly to variable-driven theming.
Implementation: Signals + Token Refresh Step-by-Step
1) Migrate Hot Paths to SignalStore
We started with the filter → table → chart pipeline. RxJS streams from the API were converted with toSignal(), and derived aggregates moved into computed(). The store held raw data, filtered views, and chart-ready series.
Example Theme + Preferences Store:
Wrap remote data in a store; convert derived selectors to computed signals.
Coalesce writes; debounce filter changes where appropriate.
Expose narrow read-only signals to components.
Store Code
import { signal, computed, effect, inject } from '@angular/core';
import { SignalStore, withState, withComputed, withMethods } from '@ngrx/signals';
interface ThemeState {
mode: 'light' | 'dark';
density: 'compact' | 'comfortable';
tokens: Record<string, string>; // CSS var values
}
const defaultTokens: Record<string,string> = {
'--color-primary-500': '#3B82F6',
'--radius-md': '8px',
'--space-2': '8px',
'--table-row-height': '40px'
};
export const ThemeStore = SignalStore(
{ providedIn: 'root' },
withState<ThemeState>({ mode: 'light', density: 'comfortable', tokens: defaultTokens }),
withComputed(({ mode, density, tokens }) => ({
isDark: computed(() => mode() === 'dark'),
rowHeight: computed(() => density() === 'compact' ? '32px' : tokens()['--table-row-height'])
})),
withMethods((store) => ({
setMode(mode: ThemeState['mode']) { store.patchState({ mode }); },
setDensity(density: ThemeState['density']) { store.patchState({ density }); },
setToken(key: string, value: string) {
const next = { ...store.state().tokens, [key]: value };
store.patchState({ tokens: next });
}
}))
);
// Apply tokens to :root once per change—no class churn
export function bindTokensToDocument(theme = inject(ThemeStore)) {
effect(() => {
const tokens = theme.state().tokens;
const root = document.documentElement.style;
for (const [k, v] of Object.entries(tokens)) root.setProperty(k, v);
root.setProperty('--row-height', theme.rowHeight());
});
}2) Tokenize the Theme
/* tokens.scss */
:root {
--color-primary-500: #3B82F6;
--color-surface: #0B1220;
--radius-md: 8px;
--space-2: 8px;
--row-height: 40px;
/* PrimeNG bridges */
--p-primary-500: var(--color-primary-500);
--p-border-radius: var(--radius-md);
}
[data-theme="dark"] {
--color-surface: #0A0F1A;
}Define tokens centrally; export as CSS variables.
Map design tokens to PrimeNG variables—no forked theme CSS.
Keep density/scale in tokens; avoid per-component utility classes.
3) Consume Signals in Components
@Component({
selector: 'au-dashboard-table',
templateUrl: './table.html',
changeDetection: 0 // default is fine; Signals minimize work
})
export class DashboardTable {
private theme = inject(ThemeStore);
rowHeight = this.theme.rowHeight; // computed signal
}<table class="data" [style.setProperty]="'--row-height,' + rowHeight()">
<!-- rows ... -->
</table>Bind styles to CSS vars—not to frequent [ngClass] toggles.
Use computed signals for small, memoized transformations.
Ensure inputs are signals or toSignal() outputs to avoid extra detection cycles.
4) Measure, Gate, and Ship
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lhci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- run: npx http-server dist -p 8080 &
- run: npx @lhci/cli autorun --upload.target=temporary-public-storage
env:
LHCI_BUILD_CONTEXT__currentBranch: ${{ github.ref_name }}Thresholds lived in a small script that failed the build if Performance < 85 on the dashboard route.
Use Angular DevTools render counts + flame charts to confirm reductions.
Lighthouse CI thresholds for INP/LCP; post results to Slack.
Feature-flag the rollout for instant rollback.
Before/After: Metrics and What Actually Changed
Render Counts (Angular DevTools)
Before, a filter touch triggered three BehaviorSubjects, which fanned out into six subscribers. We collapsed them into a single write in the store, used computed() for derived sets, and bound visuals to CSS variables so density changes never re-instantiated components.
Filter change render count: 420 → 58 (-86%).
Chart canvas re-renders: 14 → 2.
Table row reflows on density toggle: 3 → 0 (paint only).
Lighthouse + Core Web Vitals
We instrumented GA4 with custom events tagged by build SHA to confirm production parity with CI. The key win was eliminating layout-causing JS writes; tokens allowed the browser’s compositor to do its job.
Lighthouse Performance: 68 → 92.
INP: 180ms → 95ms; LCP: 2.7s → 1.8s; CLS: 0.08 → 0.02.
PrimeNG Integration
PrimeNG played nicely once we bridged p-variables to our token set. We avoided global utility toggles and relied on variables for color, spacing, and radius, so component trees stayed stable.
Mapped tokens → PrimeNG vars; no theme fork.
Disabled heavy shadows/animations behind prefers-reduced-motion.
Used cdk-virtual-scroll for large tables—kept Signals-based row density.
Team Impact
We didn’t freeze features. New chart types shipped during the refactor. The token dictionary now feeds other internal tools, including a cloud accounting dashboard and a telematics UI at an insurance tech company.
Zero-downtime rollout with feature flags in two sprints.
Developers now measure with a standard checklist and CI job.
Designers work from the same token dictionary shared across apps.
When to Hire an Angular Developer for a Signals + Token Refresh
Signals You’re Ready
If this sounds familiar, bring in an Angular consultant who has shipped Signals and token systems in production. The fix is architectural, not cosmetic. A senior Angular engineer will quantify the ROI with render counts and Core Web Vitals, then implement a safe, flagged rollout.
Lighthouse scores stuck < 80 despite CDN and image optimizations.
Angular DevTools shows hundreds of renders on a simple interaction.
Theme/density toggles cause visible layout shift or jitter.
PrimeNG/Material overrides are hard-coded and inconsistent.
Typical Timeline
For complex multi-tenant apps, we start with the heaviest route, instrument it, and generalize the store and tokens. No feature freeze required.
Discovery and measurement: 3–5 days.
Pilot route (Signals + tokens): 1–2 sprints.
Scale across app: 2–4 sprints with CI guardrails.
Code You Can Reuse: Token Bridges and Stable Layout
PrimeNG Token Bridge
/* prime-bridge.scss */
:root {
--p-content-padding: var(--space-2);
--p-focus-ring: 0 0 0 3px color-mix(in srgb, var(--color-primary-500) 35%, transparent);
--p-data-table-row-height: var(--row-height);
}
.p-datatable .p-datatable-tbody > tr {
height: var(--p-data-table-row-height);
}Avoiding Layout Thrash
Stable layout turns CLS into a non-event. Your charts should update via data changes, not DOM size changes. Signals make the data updates predictable; tokens keep the geometry steady.
Prefer CSS variables over JS writes to style/layout properties.
Batch token updates once via effect().
Keep chart canvas size stable; update only series data.
Takeaways and Next Steps
What to Instrument Next
Numbers win hearts and budgets. After the initial gains, keep performance visible in Slack and dashboards.
Render-count snapshots for top routes committed to repo.
Per-route Lighthouse baselines in CI with budgets.
GA4 events enriched with build SHA and feature-flag state.
Where I’ve Done This
Across industries—airlines, media, insurance telematics—the Signals + tokens pattern lands quick wins without upheaval.
Telecom ad analytics dashboards (this case).
Airport kiosks with offline-tolerant UX (density tokens tuned for touch).
Device management portals for an IoT hardware company (role-based themes).
Key takeaways
- Signals + SignalStore coalesced UI state and slashed re-renders 86% on filter changes.
- A tokenized theme (CSS variables) eliminated layout thrash and raised Lighthouse Performance from 68 to 92.
- PrimeNG + tokens worked cleanly by mapping design tokens to component CSS vars—no forked themes.
- DevTools flame charts, render counts, and Lighthouse CI turned subjective UX “jank” into numbers leadership understood.
- Feature flags let us ship the refactor with zero downtime and immediate rollback safety.
Implementation checklist
- Audit render counts with Angular DevTools; record flame charts for top routes.
- Identify re-render hot spots; convert BehaviorSubjects/selectors to Signals + SignalStore.
- Define a design token dictionary and export CSS variables at :root; map to PrimeNG variables.
- Replace dynamic class churn with stable CSS vars; avoid layout-causing JS writes.
- Gate rollouts with feature flags; measure INP/LCP/CLS before/after with Lighthouse and GA4.
- Add CI guardrails: bundle budgets + Lighthouse thresholds + render-count snapshot tests.
Questions we hear from teams
- How much does it cost to hire an Angular developer for a Signals + token refresh?
- Most teams see results in 2–6 weeks. Discovery and pilot range from $8k–$30k depending on scope. Full rollout varies by app size. I price fixed-scope pilots when possible and keep production stable with feature flags.
- What does an Angular consultant do in this engagement?
- I audit render counts with Angular DevTools, define a token dictionary, migrate hot paths to Signals/SignalStore, bridge tokens to PrimeNG/Material, and set up Lighthouse CI. We ship behind flags with rollback and measure wins in GA4.
- How long does an Angular upgrade or refactor like this take?
- Pilot route: 1–2 sprints. App-wide rollout: 2–4 sprints. No feature freeze. We work within your Nx monorepo and CI to avoid blocking feature delivery and maintain zero-downtime deploys.
- Do we need a design redesign to implement tokens?
- No. Tokens codify your existing system—colors, spacing, radii, density. We can gradually improve visuals later. The first goal is stability and performance without UX regression.
- Will PrimeNG or Material theming break?
- No. We map your design tokens to library variables. Components keep their APIs; the look and density come from tokens. This avoids forks and reduces maintenance.
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