
Signals + Design Tokens in the Wild: A 6‑Week Refresh That Cut Renders 68% and Lifted Lighthouse to 93 (Angular 20+)
Real case study: migrating style tokens to CSS variables, driving them with Signals + SignalStore, and taming render storms in PrimeNG dashboards—without breaking prod.
We stopped treating theme as app state. Tokens moved to CSS variables, Signals coordinated changes, and the UI stopped waking up for every style tweak.Back to all posts
A dashboard that jitters every time someone toggles dark mode. A filter drawer that triggers 400+ change detection passes when all you wanted was tighter density on rows. I’ve seen it across aviation, media, and telecom: design tokens implemented as component state instead of CSS, compounded by sprawling inputs and vibe-coded templates.
This is how we fixed it—Signals + SignalStore for token state, tokens as CSS variables, and surgical changes to PrimeNG. Six weeks later: 68% fewer renders on key routes, Lighthouse 93 desktop/88 mobile, and calmer DevTools flame charts. This is the kind of engagement I’m brought in for when teams need an Angular expert to stabilize without slowing delivery.
The Jittery Dashboard Scene: Render Storms From Harmless Theme Toggles
Context
Traffic spikes during campaign launches exposed a weird UX: toggling density or brand color would cause measurable stutter. DevTools showed repeated re-renders across unrelated widgets.
Angular 20 app in an Nx monorepo
PrimeNG-heavy analytics UI for a leading telecom provider
Feature flags via Firebase Remote Config
Challenge
Design tokens lived in app state, not CSS. Changing a token broadcasted to dozens of subscribers. Each subscriber triggered new inputs and templates, ballooning render counts. Lighthouse hovered around 68 desktop, 54 mobile, with poor TBT under synthetic CPU throttling.
Tokens were modeled as component inputs and services with Subjects
Global CSS was partial; many components re-applied classes on each change
PrimeNG theming mixed with ad-hoc SCSS variables
Why It Matters for Angular 20 Teams Shipping Today: Renders, Core Web Vitals, and Safe Rollouts
The cost of render storms
When tokens are data instead of style, the entire app participates in a theme change. That’s not a product feature—that’s a performance bug.
More change detection cycles than necessary
UI jank during scroll/zoom/interactions
Hard-to-reproduce performance regressions
KPIs we targeted
We aligned to business impact: faster dashboards, fewer support tickets during ad-flight spikes, cleaner Core Web Vitals for SEO-backed pages.
Render counts on 4 hot routes
Lighthouse (mobile + desktop)
TBT, INP, CLS under load
Crash-free sessions and interaction error rate
Signals‑Driven Token Refresh: Architecture and Code
These changes turned token updates into pure CSS operations. Signals coordinated state, but components stayed blissfully unaware—so no render storms when product toggled density or brand hue during demos.
1) Token contract as CSS variables
We stopped treating tokens as inputs. CSS variables do the heavy lifting without touching Angular’s change detection.
Keep tokens minimal: color, radius, density, spacing, type-scale
Apply on :root; use data-* attributes for mode/density
No template churn for style-only changes
Example: Base token layer
/* tokens.scss */
:root {
/* color */
--au-brand-hue: 221;
--au-bg: hsl(var(--au-brand-hue) 20% 98%);
--au-fg: hsl(221 40% 12%);
/* shape */
--au-radius-sm: 4px;
--au-radius-md: 8px;
/* density */
--au-density: 0; /* -1 compact, 0 comfy, +1 spacious */
--au-space-1: clamp(2px, 2px + var(--au-density)*1px, 6px);
--au-space-2: clamp(6px, 8px + var(--au-density)*2px, 16px);
}
/* density via attribute, zero Angular bindings */
:root[data-density="compact"] { --au-density: -1; }
:root[data-density="spacious"] { --au-density: 1; }2) Drive tokens with a small SignalStore
// theme.store.ts
import { computed, effect, signal } from '@angular/core';
export type Density = 'compact' | 'comfy' | 'spacious';
interface ThemeState { brandHue: number; radius: number; density: Density; }
export class ThemeStore {
private readonly _brandHue = signal(221);
private readonly _radius = signal(8);
private readonly _density = signal<Density>('comfy');
readonly brandHue = this._brandHue.asReadonly();
readonly radius = this._radius.asReadonly();
readonly density = this._density.asReadonly();
readonly cssVars = computed(() => ({
'--au-brand-hue': String(this._brandHue()),
'--au-radius-md': `${this._radius()}px`,
}));
constructor() {
// Apply CSS vars without causing any Angular template updates
effect(() => {
const root = document.documentElement;
const vars = this.cssVars();
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
root.setAttribute('data-density', this._density());
}, { allowSignalWrites: true });
}
setDensity(d: Density) { this._density.set(d); }
setHue(h: number) { this._brandHue.set(h); }
setRadius(px: number) { this._radius.set(px); }
}Single source of truth for tokens
Writes directly to documentElement style/attributes
No component-level token inputs
3) PrimeNG alignment without forking
/* primeng-overrides.scss */
:root {
--p-primary-color: hsl(var(--au-brand-hue) 85% 46%);
--p-content-padding: var(--au-space-2);
--p-border-radius: var(--au-radius-md);
}Map PrimeNG variables to tokens; avoid per-component bindings
Leave components ignorant of theme state
Dark mode and density become style-only toggles
4) Render-count instrumentation
// render-count.directive.ts
import { Directive, effect } from '@angular/core';
import { afterRender } from '@angular/core';
@Directive({ selector: '[renderCount]' })
export class RenderCountDirective {
private count = 0;
constructor() {
afterRender(() => {
this.count++;
if (this.count % 100 === 0) console.debug('renders:', this.count);
});
}
}Trust but verify with Angular DevTools and counters
Track ngFor churn and effects
Baseline before, compare after
5) Scope signals, not the world
<!-- before: token-driven class bindings across the grid -->
<div class="grid" [class.compact]="density() === 'compact'">
<app-tile *ngFor="let w of widgets">...</app-tile>
</div>
<!-- after: tokens on :root; no binding here; trackBy for stability -->
<div class="grid">
<app-tile *ngFor="let w of widgets; trackBy: trackWidget">...</app-tile>
</div>Split monolithic dashboards into smaller signal islands
Prefer computed per-widget over global signals
Use trackBy for ngFor; memoize expensive maps
Before/After: Telecom Analytics Dashboard Numbers
Instrumentation setup
# lighthouse-ci.yml (excerpt)
name: Lighthouse CI
on: [pull_request]
jobs:
lhci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build:prod
- run: npx http-server dist/app -p 8080 &
- run: npx @lhci/cli autorun --collect.url=http://localhost:8080Nx target to run Lighthouse CI on PRs
Angular DevTools flame chart capture on hot routes
GA4 + Firebase Performance to validate in the wild
Results (6 weeks)
Angular DevTools flame charts went from ‘orange carpets’ to neat green stacks. The moment density switched, no unrelated tiles re-rendered. Support tickets about ‘stutters during ad-flight exports’ dropped to near zero.
Render counts: −68% on dashboard route (1,340 → 425 in first 5s)
Lighthouse: 68 → 93 (desktop), 54 → 88 (mobile)
TBT: 520ms → 140ms; INP p75: 230ms → 120ms
CSS bytes: −19% by eliminating duplicated theme classes
How an Angular Consultant Approaches a Signals + Token Refresh
1) Discovery and baselining (Week 0–1)
Short, focused diagnosis—no yak shaving. If you’re looking to hire an Angular developer for a quick performance win, this is where we start.
Render-count probes added to 3 hot components
Establish Lighthouse, WebPageTest, GA4/ Firebase baselines
Risk register: components touching theme state
2) Architecture and pilot (Week 1–3)
One slice shipped to a canary audience (10%) via Firebase Remote Config. No user-facing changes, only smoother interactions.
Token contract approved; CSS variables wired on :root
Theme SignalStore stood up behind feature flag
PrimeNG override layer mapped to tokens
3) Rollout and hardening (Week 3–5)
We removed thousands of template bindings with zero functional change. CI caught any accidental reintroductions.
Widget-by-widget removal of token inputs
TrackBy and computed memoization added
Automated Lighthouse CI gate to block regressions
4) Prove it in production (Week 5–6)
We closed with measurable outcomes and a playbook your team can reuse. If you need a remote Angular developer with Fortune 100 experience to run this end-to-end, I’m available.
50% → 100% rollout
INP, TBT, and error taxonomy monitored
Executive-ready before/after report delivered
When to Hire an Angular Developer for Legacy Rescue
Explore NG Wave for production-grade animated components that already follow Signals best practices. When tokens are style-only, your feature teams move faster.
Signs you’re ready
If your theme or density updates require a component rebuild, you’re paying a render tax. A short, surgical engagement can stop the bleeding.
Design tokens exist but live in component state
Theme toggles cause stutter or layout shift
Lighthouse stuck <80 despite modern Angular
What you’ll get in 2–4 weeks
See related modernization and code-rescue work at gitPlumbers—built for teams that need to stabilize fast and keep shipping.
Token contract + CSS variable migration plan
Signals-based token store ready for audit
Before/after metrics with a rollback path
Practical Code Notes and Gotchas
For teams migrating from NgRx selectors to Signals, keep business state separate from theme state. SignalStore is great—just don’t conflate product data with tokens.
Avoid binding loops
If you must reflect state in the DOM, gate effects with shallow equality.
Don’t read signals in templates solely to style; prefer CSS variables
Keep effects idempotent; write only when values change
PrimeNG specifics
You can keep your PrimeNG upgrade path clean without forking.
Prefer global tokens over per-component scss maps
Use styleClass APIs, not [ngClass] churn on each tile
SSR and hydration
Pre-seed data-* attributes server-side to avoid flash-of-unstyled.
Ensure initial tokens are serialized for SSR
Hydration should not flip classes post-boot
Key Takeaways
- Tokens belong in CSS variables, not as Angular inputs.
- A small SignalStore coordinates token changes without waking the app.
- PrimeNG maps cleanly to tokens; no forks or heavy theming engines required.
- Instrument everything: afterRender, DevTools flame charts, Lighthouse CI, GA4.
- Roll out behind feature flags with clear metrics and rollback.
FAQs
How long does a Signals + token refresh take?
Typical engagement is 2–4 weeks for a focused slice, 6–8 weeks for full dashboards. We run canaries in week 2 and ship broadly by week 4–6 with measurable KPIs.
Do we need to go zoneless to see benefits?
No. Most gains came from moving tokens to CSS variables and scoping signals. Zoneless can help later; it’s not a prerequisite for big wins.
Will this break PrimeNG or require forking themes?
No. We map PrimeNG variables to our token contract. Components remain unaware of theme state, so future upgrades stay clean.
What does it cost to hire an Angular consultant for this?
Fixed-scope refreshes start at a 2–3 week engagement. I provide a discovery call within 48 hours and a written assessment in 5 business days.
What’s included in your deliverables?
Baseline metrics, token contract, SignalStore, PrimeNG mappings, instrumentation, rollout plan, and an executive-style before/after report.
Key takeaways
- Decoupling theme changes from component state via CSS variables reduced renders by 68% across high-traffic routes.
- A small SignalStore centralizing tokens (density, radius, brand hue) avoided global re-renders and simplified theming.
- PrimeNG theming aligned to tokens without forking; token updates became zero-render style swaps.
- Lighthouse improved from 68→93 (desktop) and 54→88 (mobile); TBT and INP stabilized under load.
- Changes shipped behind feature flags with Firebase + GA4 instrumentation and Nx canary channels—no production fires.
Implementation checklist
- Map current styles to a minimal token contract (color, radius, density, spacing, typography scale).
- Move tokens to CSS variables on :root and data-* attributes—avoid binding classes repeatedly in templates.
- Create a SignalStore to manage tokens and only write to document.documentElement when values change.
- Scope signals narrowly: computed per-context, granular components, and trackBy on ngFor for render sanity.
- Instrument: afterRender counters, Angular DevTools flame charts, Lighthouse CI, GA4/ Firebase perf events.
- Roll out behind flags; ship canary to 10%, then 50%, measure, then 100% with rollback ready.
Questions we hear from teams
- How long does a Signals + token refresh take?
- 2–4 weeks for a focused slice; 6–8 weeks for full dashboards. We canary in week 2 and ship broadly by week 4–6 with clear KPIs and rollback.
- Do we need zoneless change detection for gains?
- Not initially. Most wins came from moving tokens to CSS variables and scoping signals. Zoneless can be a phase-two optimization.
- Will PrimeNG theming break or require forks?
- No. We map PrimeNG variables to our token contract. Components don’t bind to theme state, so upgrades remain straightforward.
- How much does it cost to hire an Angular developer for this work?
- I offer fixed-scope packages starting at 2–3 weeks. Discovery call within 48 hours, with a written assessment delivered in 5 business days.
- What deliverables will we receive?
- Baseline metrics, token contract, SignalStore, PrimeNG variable map, instrumentation, rollout plan with canaries, and a before/after performance report.
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