Signals + Design Tokens in the Wild: A 6‑Week Refresh That Cut Renders 68% and Lifted Lighthouse to 93 (Angular 20+)

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:8080

  • Nx 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.

Related Resources

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.

Hire Matthew – Remote Angular Expert, Available Now See Live Angular Apps: NG Wave, gitPlumbers, IntegrityLens, SageStepper

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
NG Wave Component Library

Related resources