Signals + Design Tokens Refresh in Angular 20+: Cut Render Counts 55% and Pushed Lighthouse to 97 on a Real Dashboard

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-storage

1) 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.

Related Resources

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.

Hire Matthew – Remote Angular Expert, Available Now See how I rescue chaotic codebases with gitPlumbers (70% velocity increase)

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