
Signals + Design Tokens Refresh in Angular 20+: Cut Render Counts 55% and Pushed Lighthouse to 97 on a Real Dashboard
A focused case study: migrating theme state to Signals + a measurable design‑token layer that stopped jitter, reduced change detection, and lifted Core Web Vitals.
Render counts are a product decision. Signals + tokens make them measurable—and fixable—without a feature freeze.Back to all posts
I’ve seen the same pattern in multiple enterprises—telecom ad analytics, airline kiosks, IoT portals: theming bolted on late with global services, BehaviorSubjects, and heavy change detection. The result is jittery dashboards, laggy theme toggles, and Lighthouse scores leadership won’t put in a slide deck.
This case study documents a recent Angular 20+ refresh where I moved theme state to Signals + SignalStore and replaced ad‑hoc styles with a measured design‑token layer. We cut renders by 55% on key routes, dropped main‑thread work, and lifted Lighthouse to 97 performance / 100 accessibility—without breaking PrimeNG or bespoke controls.
Context: a high‑traffic analytics dashboard (think: a leading telecom provider). The team was mid‑roadmap for 2025 and asked if I could stabilize the UI, make density and color theming measurable, and stop the “flash” that appeared when toggling themes.
The Dashboard That Jittered: What We Saw on Day 1
Challenge
Angular DevTools flame charts showed wide purple bars across app-root every time users changed density or theme. Render counts per interaction averaged 38–52. INP measured ~290 ms on mobile mid‑range devices and LCP hovered around 3.1 s on content‑heavy routes.
Theme toggles re‑rendered entire pages
PrimeNG tables stuttered on hover/focus
Lighthouse mobile performance hovered at 78–82
Accessibility AA had ~10 color contrast fails
Why it mattered in 2025 roadmaps
With Angular 21 around the corner and budgets resetting, the team wanted a repeatable pattern: Signals‑first state, token‑driven theming, and a CI layer that stops drift. They needed an Angular consultant who could land it without a feature freeze.
Design tokens must be measurable and accessible
Signals adoption is a top‑down priority for many orgs
VPs want Core Web Vitals in OKRs
Why Angular 12‑Style Theming Breaks When You Adopt Signals
Root causes we discovered
BehaviorSubject‑driven theming triggered change detection through large portions of the tree. Mixed strategies (SCSS at build time + inline runtime styles) caused unnecessary reflows. PrimeNG overrides lived in component styles, making density and typography changes expensive.
Global theming service with BehaviorSubject next() cascades
Style recalculation on and layout thrash
Mix of SCSS variables and runtime inline styles
PrimeNG theme overrides scattered in components
Success criteria we set
We anchored the plan to numbers executives understand and developers can instrument. The bar forced us to use Signals + CSS variables and avoid anything that touches large subtrees.
No app‑wide re-render on theme change
One effect writes CSS variables at :root
Token changes flow into PrimeNG via variables
Lighthouse P>=95, A=100, INP<180ms, CLS<0.03
How an Angular Consultant Approaches a Signals + Token Refresh
// theme.store.ts (Angular 20+, @ngrx/signals)
import { inject, Injectable, effect, untracked, signal, computed } from '@angular/core';
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
export type Density = 'comfortable' | 'compact';
export type ThemeTokens = {
primary: string; // hsl or hex
surface: string;
text: string;
radius: number; // px
fontSizeBase: number; // px
density: Density;
};
const defaults: ThemeTokens = {
primary: 'hsl(222 82% 55%)',
surface: 'hsl(0 0% 100%)',
text: 'hsl(220 17% 17%)',
radius: 8,
fontSizeBase: 16,
density: 'comfortable',
};
@Injectable({ providedIn: 'root' })
export class ThemeStore extends signalStore(
withState({ tokens: signal<ThemeTokens>(defaults) }),
withMethods((store) => ({
setTokens(update: Partial<ThemeTokens>) {
patchState(store, (s) => ({ tokens: { ...s.tokens(), ...update } }));
},
toggleDensity() {
const next = store.tokens().density === 'comfortable' ? 'compact' : 'comfortable';
patchState(store, (s) => ({ tokens: { ...s.tokens(), density: next } }));
},
}))
) {
readonly cssVars = computed(() => {
const t = this.tokens();
return new Map<string, string>([
['--color-primary', t.primary],
['--color-surface', t.surface],
['--color-text', t.text],
['--radius-md', `${t.radius}px`],
['--font-size-base', `${t.fontSizeBase}px`],
['--density-scale', t.density === 'compact' ? '0.9' : '1']
]);
});
// Single writer: apply CSS variables efficiently
private apply = effect(() => {
const vars = this.cssVars();
untracked(() => {
const root = document.documentElement.style;
vars.forEach((v, k) => root.setProperty(k, v));
});
});
}/* theme.bridge.scss */
:root {
/* App tokens */
--color-primary: hsl(222 82% 55%);
--color-surface: #fff;
--color-text: #141a1f;
--radius-md: 8px;
--font-size-base: 16px;
--density-scale: 1; // compact = 0.9
/* PrimeNG mapping */
--p-primary-color: var(--color-primary);
--p-text-color: var(--color-text);
--p-border-radius: var(--radius-md);
--p-font-size: var(--font-size-base);
}
.app-root {
font-size: var(--font-size-base);
color: var(--color-text);
background: var(--color-surface);
--control-scale: var(--density-scale);
transform: scale(var(--control-scale));
transform-origin: top left;
}<!-- density-toggle.component.html -->
<button pButton type="button" (click)="theme.toggleDensity()" label="Toggle density"></button>
<!-- Example of class-only change without forcing re-render of children -->
<div class="table-host" [class.compact]="(theme.tokens().density) === 'compact'">
<p-table [value]="rows"> ... </p-table>
</div># .github/workflows/lh-ci.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- run: pnpm i
- run: pnpm nx run web:build:production
- run: npx http-server dist/apps/web -p 4200 & sleep 2
- run: npx @lhci/cli autorun --upload.target=temporary-public-storage1) Model tokens as state with SignalStore
We created a typed ThemeStore using @ngrx/signals SignalStore. All token reads are signals; writes go through reducers. Derived signals compute CSS values and semantic tokens (surface, text, focus rings).
2) Emit tokens to CSS variables in a single effect
A single effect sets variables on :root. We wrap DOM writes with untracked to avoid effect feedback loops. This guarantees component renders aren’t triggered by the act of styling.
3) Bridge to PrimeNG and custom components
PrimeNG already consumes CSS variables. We mapped our tokens to its --p-* variables and replaced component‑level overrides with theme-scoped variables.
4) Guardrails in Nx + CI
We added Lighthouse CI, Pa11y, and a render-count smoke test using Angular DevTools scripted runs in GitHub Actions. Canary releases went to Firebase Hosting previews.
Before/After: Metrics That Execs and Engineers Believe
Angular DevTools (render counts)
We instrumented scripted interactions and captured median render counts across five runs using Angular DevTools. Reduction was consistent even under websocket data churn.
Dashboard route: 48 → 21 renders (-56%)
Table filter interaction: 32 → 14 renders (-56%)
Lighthouse (mobile)
Core Web Vitals moved: LCP 3.1 s → 2.1 s (-32%), INP 290 ms → 170 ms (-120 ms), CLS 0.05 → 0.01. Script evaluation time dropped ~18% after removing redundant theming code paths.
Performance: 82 → 97
Accessibility: 95 → 100
Best Practices: 96 → 100
SEO: 100 → 100
Production guardrails
We pushed canaries to stakeholders safely and prevented token regressions through CI checks and visual diffs. No outages; zero downtime.
Firebase Hosting previews for canary URLs
Pa11y/axe zero contrast fails
Nx affected:libs to keep builds snappy
Rollout Tactics and Risk Controls
Feature flags + remote config
We used a boolean in Firebase Remote Config to gate the new ThemeStore. A tiny adapter translated legacy SCSS variables to our CSS vars during the transition.
Flag new theme engine at 10% → 50% → 100%
Fallback to legacy theme on error signal
Accessibility + tokens
Tokens included semantic states (focus, hover) and respected prefers-reduced-motion. We also mapped text/link sizes to tokens for predictable scaling.
WCAG AA at 4.5:1 contrast enforced via tokens
Focus rings and motion preferences from tokens
What we did not do
The win came from less work, not more. Signals let us be surgical and measurable.
No global ChangeDetectorRef tick() calls
No mass QueryList style rewrites
When to Hire an Angular Developer for Legacy Rescue
Signals adoption stalled?
If your team struggles to move from Subjects to Signals or your PrimeNG/Material theme drifts per sprint, bring in an Angular consultant to land the pattern and set up guardrails.
zone.js everywhere and Subject cascades
Theme toggles cause jitter or layout shift
Enterprise constraints
I’ve landed similar work in high‑risk contexts—airport kiosks (offline‑tolerant), broadcast scheduling UIs, and telecom analytics. We can stabilize without halting delivery.
No feature freeze available
Multiple brands/tenants require strict tokens
How an Angular Consultant Approaches Signals Migration
Discovery (1 week)
We start with a measurable baseline. The output is a short plan with code diffs and guardrails.
Instrument render counts and flame charts
Token inventory + accessibility audit
Delivery (2–4 weeks)
Most teams see wins in week 1; full rollout lands by week 3–4 with training.
ThemeStore + CSS var bridge
PrimeNG mapping + component cleanup
CI: Lighthouse/Pa11y + Firebase canaries
Handover
Your team owns the system; I stay on call for spikes.
Docs + Storybook tokens showcase
Playbooks: adding a new brand in <60 minutes
Tangible Outcomes and What to Instrument Next
Outcomes
The dashboard felt instantly more stable. Support tickets dropped, and product finally had numbers to share at QBRs.
-56% renders on key pages
Lighthouse P97/A100
Fewer UI bug reports post‑release
Next steps
On other programs—employee tracking for a global entertainment company, telematics dashboards for an insurer—tying charts to tokens and watching INP in GA4 paid additional dividends.
Extend tokens to charts (D3/Highcharts)
Add INP alerts in GA4/Firebase Analytics
Document token contribution flow in Nx
Questions I Get from Directors and Recruiters
Will this work with multi‑tenant apps?
Yes. Each tenant inherits a base token set; per‑tenant diffs hydrate the ThemeStore at runtime. Scoped CSS variables prevent cross‑tenant bleed.
Does Signals outperform OnPush?
They’re complementary. Signals + fine‑grained bindings reduce change detection work; OnPush remains a good default for components.
PrimeNG vs. Angular Material?
Both work. PrimeNG ships a thorough CSS variable API, which made this refresh straightforward. Material’s tokens are evolving; we can map them too.
Key takeaways
- Moving theming and density to Signals + SignalStore eliminated global re-renders and dropped render counts by 55%.
- CSS variables driven by token signals let PrimeNG + custom components update instantly with minimal DOM writes.
- Lighthouse scores climbed to 97 performance / 100 accessibility with better LCP and 120 ms faster INP.
- Guardrails with Nx, Lighthouse CI, and Pa11y prevent token drift and keep UX budgets honest.
- Feature‑flagging the rollout via Firebase enabled canary testing without risking production.
Implementation checklist
- Capture a baseline with Angular DevTools render counts and Lighthouse (mobile).
- Model tokens (color, typography, radius, density) as a typed SignalStore.
- Emit tokens to CSS variables with a single effect; avoid cascading effects with untracked.
- Map token variables to PrimeNG’s CSS API and custom components.
- Gate rollout behind a Firebase flag; measure INP/LCP and re‑render deltas.
- Lock in guardrails: Lighthouse CI, Pa11y, and bundle budgets in Nx/Actions.
Questions we hear from teams
- How much does it cost to hire an Angular developer for a Signals + token refresh?
- Typical engagements run 2–4 weeks. Most teams budget for a fixed discovery (1 week) plus a 1–3 week implementation phase. I offer transparent scopes and remote delivery so you only pay for outcomes.
- What does an Angular consultant actually deliver here?
- A typed ThemeStore, CSS variable bridge, PrimeNG mapping, CI guardrails (Lighthouse/Pa11y), and a short playbook for adding brands safely. You also get before/after metrics—render counts, LCP/INP, and accessibility results.
- How long does an Angular upgrade or migration to Signals take?
- For theming/state only, expect 2–4 weeks. Full Angular version upgrades vary 4–8 weeks depending on dependencies and test coverage. I use Nx, Firebase previews, and canary deploys for zero downtime.
- Can we keep NgRx and still adopt Signals?
- Yes. Many teams use NgRx for effects/server state and Signals for UI state. @ngrx/signals SignalStore bridges both, reducing boilerplate while keeping DevTools guardrails.
- What’s involved in a typical engagement?
- Discovery in 5–7 days, implementation 1–3 sprints, and handover with training. Discovery call within 48 hours, and a written assessment in one week with code samples and a rollout plan.
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