
Inside a Signals + Design Token Refresh: 68% Fewer Renders and +18 Lighthouse Points in Angular 20
How I stabilized a jittery enterprise dashboard by migrating state to Signals/SignalStore and rebuilding tokens as CSS variables—measured with DevTools, Lighthouse, and CI.
“We didn’t change the brand. We changed where the decisions live—out of the component tree. That’s how you cut renders without cutting UX.”Back to all posts
I’ve been brought into a few Angular 20+ dashboards lately where the UI visibly jitters under load. One recent rescue (ads analytics for a telecom) combined a Signals migration with a design token refresh. The goal: stop cascading re-renders and clean up Lighthouse without regressing accessibility or brand.
This is the playbook I ran, with real measurement: render counts down 68%, INP from 320→140ms, LCP from 2.8→1.7s, and Lighthouse Performance +18. Tools: Angular DevTools, Signals/SignalStore, PrimeNG, Nx, Firebase, and Lighthouse CI.
If you’re evaluating whether to hire an Angular developer or bring in a senior Angular consultant for a stabilization sprint, here’s exactly what we did and how we proved it.
The Dashboard That Jittered: Signals + Tokens to the Rescue
Challenge: A high-traffic ads analytics dashboard (think a leading telecom provider scale) was suffering from visible card jitter, slow filters, and inconsistent theming. Lighthouse hovered in the low 70s; INP was spiky (~320ms p95).
Intervention: I migrated the hot paths to Signals/SignalStore, rebuilt the design tokens as CSS variables with a single document-level application effect, and mapped those tokens cleanly into PrimeNG. We removed per-component style overrides and stopped token swaps that caused layout shift.
Result: Render counts on the primary grid dropped 68% (Angular DevTools), Lighthouse Performance rose +18 points, INP fell 56% (320→140ms), and LCP improved 39% (2.8→1.7s). We kept AA contrast, density controls, and brand consistency.
Why Render Counts Explode in Angular Dashboards (and How Tokens Make It Worse)
As companies plan 2025 Angular roadmaps, you can’t afford re-renders on every filter change or theming action. Signals shrink the blast radius. Tokens—if implemented with CSS variables and applied via a single effect—prevent reflow storms.
Common render amplifiers in enterprise Angular
In media, aviation, and telecom dashboards I’ve built (a broadcast media network scheduling, United airport kiosks, Charter analytics), the same pattern shows up: a small state change wakes half the tree. Signals let us slice state surgically, so only the components that need to react, do.
AsyncPipe everywhere without memoized slices
Mutable inputs and object-literal bindings that change every tick
Cross-cutting services emitting broad events (theme/user/filters)
TrackBy missing or unstable keys in large *ngFor lists
How token swaps trigger layout shift
Design tokens are great, but if they’re not stabilized as CSS variables and applied consistently, you pay in CLS/INP. Theme switching should update paints, not layout. Tokens must map 1:1 to component CSS vars, not cascade through ad-hoc SCSS.
Per-component SCSS overrides competing with library themes
Class toggles that change font-size/line-height mid-render
Late-loading brand kits that reflow critical UI
How an Angular Consultant Approaches a Signals + Design Token Refresh
Below is a simplified version of the code used to make this safe and measurable.
1) Baseline and guardrails
I never touch code before capturing the current state. On this project we pinned a Lighthouse CI job in Nx + GitHub Actions and added simple render counters to the grid and filter components.
Angular DevTools flame charts + render counts on hot components
Lighthouse runs (desktop + mobile) captured in CI for before/after diffs
GA4/Firebase Performance for real-user metrics (INP/LCP/CLS)
2) Slice state with Signals/SignalStore
We kept NgRx where it added value, but moved the read-path to Signals using typed selectors. SignalStore gave us an ergonomic, co-located state module per feature without re-rendering the whole tree.
Replace broad Observables with narrow signals
Use computed() to derive minimal slices
effects() to sync with URL, storage, and document-level tokens
3) Token bridge to CSS variables
The token application runs outside the component tree, so theme changes don’t trigger component updates. PrimeNG already respects CSS vars; we aligned our tokens to its variable names.
Create a canonical token map (color, radius, spacing, motion, density)
Apply tokens once at documentElement via effect
Map tokens to PrimeNG/Material variables; delete per-component overrides
4) Verify and ship safely
We shipped the token layer behind a flag, then moved feature by feature to Signals. No big-bang release; steady deltas with measurable wins.
Visual regression on key flows; density + AA contrast gates
Feature flag rollout (Firebase Remote Config)
Lighthouse CI performance budget with pass/fail thresholds
Code Walkthrough: SignalStore Theme + Token CSS Variables and Render-Safe Components
// theme.store.ts
import { Injectable, effect, signal, computed } from '@angular/core';
export type Density = 'compact' | 'cozy' | 'comfortable';
export interface ThemeTokens {
primary: string;
surface: string;
text: string;
radiusSm: string;
radiusMd: string;
spacingSm: string;
spacingMd: string;
motion: 'on' | 'reduced';
density: Density;
}
@Injectable({ providedIn: 'root' })
export class ThemeStore {
private readonly _tokens = signal<ThemeTokens>({
primary: '#1B6EF3',
surface: '#ffffff',
text: '#111827',
radiusSm: '4px',
radiusMd: '8px',
spacingSm: '0.5rem',
spacingMd: '1rem',
motion: 'on',
density: 'cozy',
});
readonly tokens = computed(() => this._tokens());
// Apply tokens once at the document root; no component rerenders.
private readonly _apply = effect(() => {
const t = this.tokens();
const root = document.documentElement;
root.style.setProperty('--color-primary', t.primary);
root.style.setProperty('--color-surface', t.surface);
root.style.setProperty('--color-text', t.text);
root.style.setProperty('--radius-sm', t.radiusSm);
root.style.setProperty('--radius-md', t.radiusMd);
root.style.setProperty('--space-sm', t.spacingSm);
root.style.setProperty('--space-md', t.spacingMd);
root.style.setProperty('--motion-enabled', String(t.motion === 'on'));
root.setAttribute('data-density', t.density);
});
setTokens(partial: Partial<ThemeTokens>) {
this._tokens.update((t) => ({ ...t, ...partial }));
}
}/* tokens.scss */
:root {
--color-primary: #1B6EF3;
--color-surface: #ffffff;
--color-text: #111827;
--radius-sm: 4px;
--radius-md: 8px;
--space-sm: 0.5rem;
--space-md: 1rem;
}
/* Density controls consumed by layout utilities */
:root[data-density='compact'] { --space-sm: 0.25rem; --space-md: 0.5rem; }
:root[data-density='comfortable'] { --space-sm: 0.75rem; --space-md: 1.25rem; }
/* PrimeNG mapping (example) */
:root {
--primary-500: var(--color-primary);
--surface-ground: var(--color-surface);
--text-color: var(--color-text);
--border-radius: var(--radius-sm);
}// grid.component.ts
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
interface Row { id: string; impressions: number; spend: number; campaign: string; }
@Component({
selector: 'app-grid',
templateUrl: './grid.component.html',
styleUrls: ['./grid.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GridComponent {
rows = input<Row[]>([]); // signal input in Angular 17+
filter = signal('');
readonly filtered = computed(() => {
const q = this.filter().toLowerCase();
const rows = this.rows();
if (!q) return rows;
return rows.filter((r) => r.campaign.toLowerCase().includes(q));
});
// Stable trackBy prevents DOM teardown
trackById = (_: number, r: Row) => r.id;
}<!-- grid.component.html -->
<input type="search" [value]="filter()" (input)="filter.set(($event.target as HTMLInputElement).value)" placeholder="Filter campaigns" />
<p-table [value]="filtered()" [rows]="25" [paginator]="true" [lazy]="false" [rowTrackBy]="trackById">
<ng-template pTemplate="header">
<tr>
<th>Campaign</th>
<th>Impressions</th>
<th>Spend</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr>
<td>{{ row.campaign }}</td>
<td>{{ row.impressions | number }}</td>
<td>{{ row.spend | currency:'USD' }}</td>
</tr>
</ng-template>
</p-table># .github/workflows/lhci.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lhci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx nx build web --configuration=production
- run: npx http-server dist/apps/web -p 4200 &
- run: npx @lhci/cli autorun --collect.url=http://localhost:4200 --upload.target=temporary-public-storageThese patterns eliminated object identity churn, reduced fan-out with computed selectors, and applied tokens once—preventing layout-shifting theme updates.
SignalStore for design tokens
A minimal SignalStore that centralizes tokens and applies them once to documentElement.
Render-safe component patterns
Use computed slices, trackBy, and avoid object-literal bindings that change identity each render.
CI guardrails with Lighthouse
Automate comparison runs in pull requests so performance can’t silently regress.
When to Hire an Angular Developer for a Signals + Token Refresh
If you’re weighing whether to hire an Angular expert or a contractor, this kind of focused refresh is ideal for short, high-impact sprints with measurable ROI.
Signs you need help
Dashboards jitter during filter changes or theme toggles
Lighthouse < 85 despite CDN and image optimization
AA contrast or density varies between pages/components
DevTools shows broad render fan-out on small state changes
What I deliver in 2–4 weeks
I’ve run this play at a global entertainment company (employee/payments), Charter (ads analytics), a broadcast media network (VPS scheduling), an insurance technology company (telematics), and an enterprise IoT hardware company (device mgmt). If you need a remote Angular consultant to stabilize quickly, this is a repeatable engagement.
Baseline report + flame charts + prioritized fixes
Signals/SignalStore slices for hot paths with tests
Token bridge mapped to PrimeNG/Material variables
CI guardrails (Lighthouse CI, bundle budgets, GA4/Firebase wiring)
Measurable Results and What to Instrument Next
Keep the loop tight: measure, change, verify. Then lock the gains with CI budgets and design system docs so they don’t drift.
Before → After (real numbers)
Render counts: −68% on primary grid (DevTools)
INP: 320ms → 140ms p95 (Firebase Performance)
LCP: 2.8s → 1.7s p95 (Lighthouse + RUM)
Lighthouse Performance: +18 points (mobile)
CLS: 0.05 → 0.01 by eliminating token-induced reflow
What to instrument next
at a major airline, we paired kiosk UX metrics with device state and exponential retry telemetry; the same rigor applies here—metrics guide every refactor.
Feature-level dashboards in GA4 (filter usage, theme toggles)
Error budgets with Sentry + OpenTelemetry traces for slow paths
Periodic Lighthouse CI with branch diff comments
FAQs: Hiring and Technical Details
If you need deeper modernization (AngularJS/JSP migration, SSR, or multi-tenant isolation), I also run broader code rescue efforts via gitPlumbers with CI/CD and upgrade playbooks.
How long does a refresh take?
Typical engagement: 2–4 weeks for a focused Signals + token refresh on a single dashboard. Larger portfolios run 4–8 weeks with staged rollouts. I start with a discovery call within 48 hours and deliver a baseline assessment within 5–7 business days.
Will this break our PrimeNG/Material theme?
No. We map your design tokens to PrimeNG/Material variables and remove conflicting per-component overrides. Density, typography, and AA contrast are validated with visual regression and automated checks.
What if our app mixes NgRx, services, and Signals?
That’s normal. Keep NgRx for writes and effects; feed the read path with Signals and computed selectors. SignalStore helps co-locate state per feature without a rewrite.
Key takeaways
- Baseline with Angular DevTools + Lighthouse before touching code; lock metrics in CI.
- Replace broad Observable subscriptions with Signals and computed slices to cut render fan-out.
- Move design tokens to CSS variables and eliminate layout-shifting token swaps.
- Map tokens to PrimeNG/Material variables; remove per-component style overrides.
- Prove ROI: render counts −68%, INP −56%, LCP −39%, Lighthouse +18.
Implementation checklist
- Capture a perf baseline (Lighthouse, Angular DevTools, GA4, Firebase Performance).
- Add render counters to hot components; record fan-out with flame charts.
- Introduce SignalStore for theme + app state; replace broad async pipes with computed signals.
- Refactor tokens to CSS variables and apply via a single document-level effect.
- Map tokens to PrimeNG and remove unstable layout-affecting class toggles.
- Add Nx + GitHub Actions job for Lighthouse CI and bundle budgets.
- Ship feature-flagged (Firebase Remote Config) and monitor Core Web Vitals.
Questions we hear from teams
- How much does it cost to hire an Angular developer for a refresh?
- For a focused Signals + token engagement, expect 2–4 weeks of senior engineering. I price per outcome with a fixed scope and measurable metrics (Lighthouse, INP, render counts). Discovery call is free; assessment is delivered in 5–7 days.
- What does an Angular consultant do in week one?
- Baseline metrics (DevTools, Lighthouse, GA4/Firebase), identify hot components, add render counters, and produce a prioritized plan. We agree on budgets and CI gates before touching features.
- How long does an Angular upgrade or migration take?
- Upgrades vary by dependencies. A straight Angular 16→20 update with UI library alignment often takes 3–6 weeks. This case study focuses on Signals + tokens; we can combine both with feature flags to avoid downtime.
- Will Signals force us to drop RxJS/NgRx?
- No. We often keep RxJS/NgRx for effects and server writes while moving the read-path to Signals and computed selectors. The goal is fewer renders and deterministic views, not a full rewrite.
- What’s involved in a typical engagement?
- Discovery call in 48 hours, baseline assessment in a week, then 2–4 weeks of iterative changes behind flags. CI adds Lighthouse and bundle budgets; we track INP/LCP/CLS in GA4/Firebase with weekly reports.
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