Signals + Design Tokens in Angular 20: Cut Render Counts 71% and Lift Lighthouse Mobile 72→94

Signals + Design Tokens in Angular 20: Cut Render Counts 71% and Lift Lighthouse Mobile 72→94

A focused case study: migrating to Signals + refreshing design tokens, integrating PrimeNG, and gating Lighthouse to deliver measurable UX wins—without a rewrite.

We cut render counts by 71% and moved Lighthouse Mobile from 72 to 94—no rewrite, no downtime.
Back to all posts

I’ve been the person on the call when a dashboard jitters during a demo. In this case study, we cut render counts by 71% and moved Lighthouse Mobile from 72 to 94 by pairing Angular 20 Signals with a pragmatic design token refresh—no rewrite, no downtime.

The Dashboard That Jittered—Then Snapped to 94

Challenge

A leading telecom provider’s analytics dashboard was jittering when users changed filters or toggled themes. Angular DevTools showed 8–12 component re-renders per change on the main view. Mobile Lighthouse sat at 72, INP hovered around 280 ms, and CLS spiked during theme changes. The ask: fix it without a rewrite and keep shipping weekly.

  • Janky filters and theme toggles

  • High re-renders on table/chart screens

  • Mobile Lighthouse at 72

Context

Stack was Angular 20 in an Nx repo with PrimeNG and Highcharts/D3. CI shipped Firebase preview channels per PR, but performance wasn’t gated. The code leaned on @Input chains, RxJS merges, and ad‑hoc theme SCSS—classic enterprise accretion.

  • Angular 20 + PrimeNG

  • Nx workspace, Firebase previews

  • Heavy table + chart views

Goal

We targeted the top two routes by traffic. The plan: move derived state to Signals/SignalStore, refresh design tokens to CSS variables, and gate Lighthouse in CI.

  • Reduce render counts

  • Lift Lighthouse > 90 Mobile

  • Zero downtime

Why Angular 20+ Teams Should Pair Signals with Design Tokens

Signals tame renders

Signals and computed selectors keep change detection scoped to real dependencies. When coupled with SignalStore, derived state stops thrashing the view tree.

  • Localize dependencies

  • Drop accidental change cascades

Tokens tame paints

Design tokens as CSS variables let the browser resolve theme changes in a single pass. No recompiling SCSS, no global recalcs. Paired with prefers-reduced-motion and consistent z/elevation tokens, CLS and INP drop.

  • 1-pass theme swaps

  • GPU-friendly transforms

Implementation: Signals Store, PrimeNG, and Token Refresh

1) Baseline and guardrails

We profiled the hot routes with Angular DevTools and recorded render counts per user action. We added Lighthouse CI to GitHub Actions with thresholds so regressions would fail PRs.

  • Angular DevTools profiles

  • Lighthouse CI on PRs

2) Migrate derivations to Signals/SignalStore

We replaced input-driven chains with a SignalStore that owns filters, paging, and chart transforms. Downstream components read signals and re-render only when their slice changes.

  • Replace chatty @Input chains

  • Move combineLatest logic to computed signals

3) Template fixes

We added trackBy, split large templates into smaller components, and enabled PrimeNG virtualScroll for data-heavy tables.

  • trackBy for lists

  • Virtual scroll for tables

4) Design tokens refresh

We consolidated colors/typography/density into tokens exposed as CSS variables. Theme toggles use a data-theme attribute on to swap values without layout thrash. Motion tokens respect prefers-reduced-motion.

  • Color, spacing, typography, density

  • Motion tokens with reduced-motion

5) PrimeNG theming

PrimeNG accepts CSS variable overrides; we avoided deep selectors and mapped tokens to Prime variables.

  • Override at variable level

  • Keep component CSS untouched

6) Verify and ship

We re-ran Angular DevTools and Lighthouse, then shipped behind Firebase preview channels for stakeholder sign-off.

  • Re-profile renders

  • Compare Lighthouse before/after

Code Walkthrough: SignalStore Selectors and Token Mapping

SignalStore for dashboard state

Here’s the trimmed store that eliminated our worst render cascades:

import { signal, computed, inject } from '@angular/core';
import { SignalStore, withMethods, withState } from '@ngrx/signals';
import { toSignal } from '@angular/core/rxjs-interop';
import { switchMap, map } from 'rxjs/operators';
import { AnalyticsApi } from './analytics.api';

interface State {
  dateRange: { from: Date; to: Date };
  region: string | null;
  page: number;
}

export const DashboardStore = SignalStore
  .provide(withState<State>({
    dateRange: { from: new Date(Date.now() - 7*864e5), to: new Date() },
    region: null,
    page: 1
  }))
  .provide(withMethods((store, api = inject(AnalyticsApi)) => ({
    setDateRange: (r: State['dateRange']) => store.patchState({ dateRange: r, page: 1 }),
    setRegion: (region: string | null) => store.patchState({ region, page: 1 }),
    setPage: (page: number) => store.patchState({ page }),

    // Signals
    dateRange: signal(store.state().dateRange),
    region: signal(store.state().region),
    page: signal(store.state().page),

    // Derived signals
    query: computed(() => ({
      from: store.state().dateRange.from.toISOString(),
      to: store.state().dateRange.to.toISOString(),
      region: store.state().region,
      page: store.state().page
    })),

    // Server data as signal (via RxJS interop)
    rows: toSignal(store.query().pipe(
      switchMap(q => api.fetchRows(q)),
    ), { initialValue: [] as any[] }),

    chartSeries: computed(() => {
      const rows = store.rows();
      // Avoid reformatting when filters unchanged
      return groupBySeries(rows);
    })
  })));

  • Filters/paging as signals

  • Derived chart data as computed

Template with trackBy and signal reads

We kept bindings narrow and added trackBy to kill churn on large lists:

<p-table
  [value]="store.rows()"
  [virtualScroll]="true"
  [rows]="50"
  [scrollHeight]="'60vh'"
  [trackBy]="trackById">
  <ng-template pTemplate="header">
    <!-- header content -->
  </ng-template>
  <ng-template pTemplate="body" let-row>
    <tr>
      <td>{{ row.id }}</td>
      <td>{{ row.region }}</td>
      <td>{{ row.impressions | number }}</td>
    </tr>
  </ng-template>
</p-table>

  • No async pipe churn

  • Minimal bindings

Design tokens as CSS variables

Tokens live in a single file, applied by theme attribute. PrimeNG reads the same variables:

/* tokens.scss */
:root {
  --color-bg: #0b0c10;
  --color-surface: #111318;
  --color-text: #e8eaed;
  --brand-500: #4f8cff;
  --radius-sm: 6px;
  --density: 0.875; // compact
  --motion-duration-1: 120ms;
}

:root[data-theme='light'] {
  --color-bg: #ffffff;
  --color-surface: #f7f8fa;
  --color-text: #1f2937;
  --brand-500: #2b6cff;
}

/* PrimeNG bridge */
:root {
  --primaryColor: var(--brand-500);
  --surfaceGround: var(--color-bg);
  --surfaceCard: var(--color-surface);
  --textColor: var(--color-text);
  --borderRadius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 1ms !important; transition-duration: 1ms !important; }
}

  • data-theme swaps

  • PrimeNG variable mapping

Theme toggle without layout thrash

The toggle writes one attribute; everything else is CSS-driven:

// theme.service.ts
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  theme = signal<'dark' | 'light'>('dark');
  apply(theme: 'dark' | 'light') {
    this.theme.set(theme);
    document.documentElement.setAttribute('data-theme', theme);
  }
}

  • Single attribute write

  • No recalcs cascade

Measurable Results: Lighthouse, Web Vitals, and Render Counts

Before → After

Across the two highest-traffic routes, CPU time dropped 38%, and we removed 45% of duplicate effect work. Theme toggles no longer nudge layout, and the table route feels instantaneous thanks to virtualization and trackBy.

  • Mobile Lighthouse: 72 → 94

  • INP: 280 ms → 120 ms (-57%)

  • CLS: 0.12 → 0.01

  • Re-renders on main route: 8–12 → 2

Team impact

We kept weekly releases, shipped behind Firebase preview channels, and added Lighthouse gates so perf wins don’t erode. Stakeholders finally saw numbers in PRs, not just adjectives.

  • No rewrite, no downtime

  • Confidence via CI gates

How an Angular Consultant Approaches a Signals Migration

Playbook

I isolate the top two user flows by revenue/traffic, prototype Signals/SignalStore as a drop-in for a single route, and measure the delta with DevTools and Lighthouse. If the numbers hold, we scale across the app and lock it with gates.

  • Profile → Prioritize → Prototype → Prove

  • Protect wins with CI

No-rewrite ethos

In telecom, media, and airline projects, we never stop delivery. Signalized slices coexist with existing RxJS flows until de-risked. Feature flags and preview channels keep stakeholders comfortable.

  • Surgical refactors

  • Feature flagging

When to Hire an Angular Developer for a Tokens + Signals Refresh

Signals you’re ready

If your dashboard jitters on theme changes, filters feel sticky, or Lighthouse Mobile sits under 90, a short engagement (2–4 weeks) can land measurable gains without touching every component.

  • Janky theme toggles or density switches

  • Render counts > 5 per interaction

  • Mobile Lighthouse < 90

What you’ll get

Expect a crisp report, targeted PRs, and documented guardrails. If you need deeper stabilization, I partner with gitPlumbers to rescue chaotic code without freezing delivery.

  • Before/after report with numbers

  • PRs with Signals + tokens

  • CI gates to preserve gains

Related Resources

Key takeaways

  • Signals + SignalStore reduce accidental re-renders by localizing change detection to real dependencies.
  • Design tokens (CSS variables) make theming changes 1-pass and GPU-friendly—no app-wide recalcs.
  • PrimeNG integrates cleanly with tokens—override theme variables and respect prefers-reduced-motion.
  • Instrument before/after: Angular DevTools render counts + Lighthouse/Core Web Vitals with CI gates.
  • You don’t need a rewrite—surgical refactors and a token refresh can land double‑digit Lighthouse gains.

Implementation checklist

  • Baseline: record Lighthouse M/D, FCP, INP, CLS; capture Angular DevTools render counts for hot paths.
  • Replace chatty @Input chains with Signals + computed selectors; move derived logic to a SignalStore.
  • Introduce trackBy, vDom slicing, and data virtualization for heavy tables/charts.
  • Refresh design tokens: color, elevation, density, typography; wire to CSS variables and PrimeNG.
  • Add prefers-reduced-motion fallbacks and font-display: swap; preconnect to font/CDN origins.
  • Automate Lighthouse CI gates and GA4 dashboards for post‑deploy verification.

Questions we hear from teams

How long does a Signals + design token refresh take?
Typical engagements run 2–4 weeks for one or two high‑impact routes. Larger dashboards span 4–8 weeks. We ship behind preview environments and keep weekly releases without downtime.
Do we need to rewrite our Angular app to use Signals?
No. Signals and SignalStore can be introduced surgically alongside existing RxJS and NgRx code. We start with a slice, prove wins with Lighthouse and DevTools, then scale.
How much does it cost to hire an Angular developer for this work?
It varies by scope and team maturity. Most 2–4 week focused refreshes are a fraction of a rewrite and return immediate UX gains. Book a discovery call for a tailored estimate.
Will PrimeNG work with design tokens and Signals?
Yes. PrimeNG plays well with CSS variable tokens and virtual scroll. Signals localize updates; tokens make theme changes cheap. Together they deliver smoother interactions and better Lighthouse scores.
What metrics do you instrument to prove success?
Angular DevTools render counts, Lighthouse Performance/INP/CLS, GA4 events with CI metadata, and user-timing marks around critical interactions. Gates in CI prevent regressions on future PRs.

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 Need Code Rescue First? See gitPlumbers

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