When agents use itUse this whenever the user says an Angular app is slow, asks about OnPush, zoneless, signals for performance, @defer, hydration, lazy loading, trackBy/track, CDK virtual scroll, bundle budgets, or profiling with Angular DevTools. Also apply it when reviewing Angular code that uses function calls or pipes with heavy logic in templates, `*ngFor` without track, default change detection, or manual subscriptions.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill angular-performance -a github-copilot
# or with the org installer (adds .github/skills/angular-performance):
npx -y github:AGCO-Global/org-skills add skill angular-performance
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-angular-skills plugin, which bundles all Frontend / Angular skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-angular-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill angular-performance -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill angular-performance -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-performance -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-performance -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
Angular Performance
Angular performance problems cluster into three buckets: change detection doing too much work, too much JavaScript shipped, and templates doing work on every check. Identify the bucket before proposing a fix.
1. Measure
Symptom
Tool
Metric
Interactions feel slow
Angular DevTools → Profiler (record, see CD cycles and per-component time)
Number of change-detection runs per interaction, longest component
Slow load
Lighthouse, ng build --stats-json + esbuild analyzer / source-map-explorer
LCP, initial bundle size, number of lazy chunks
Jank while scrolling/typing
Chrome Performance panel
Long tasks, time in tick() / refreshView
Memory growth across routes
DevTools heap snapshots
Retained components, subscriptions
Layout shift
Lighthouse CLS
Images/fonts without dimensions, late-loaded content
Capture a baseline with numbers before changing anything. Profile a production build (ng build, ng serve --configuration production), not dev.
2. Change detection — the biggest lever
Work down this list:
ChangeDetectionStrategy.OnPush on every component. Default CD checks the entire tree on every event; OnPush checks a component only when an input reference changes, an event fires inside it, or a signal it reads changes. Add it, then fix what breaks — breakage means something is mutating objects in place.
Signals for all component state. With signals, Angular marks only the exact components that read a changed signal. Convert @Input() → input(), fields → signal(), derived → computed().
Go zoneless.provideZonelessChangeDetection() removes zone.js: no more CD on every setTimeout, every fetch, every mouse move. Requires OnPush + signals everywhere; third-party libs must not rely on zone. This is the end state for new apps.
Kill template function calls.{{ formatPrice(item.price) }} runs on every CD cycle. Replace with a pure pipe (memoised per input) or a computed().
Pure pipes over impure. Never pure: false unless the pipe genuinely depends on external mutable state; use signals instead.
@for with track on a stable id. track $index on mutable lists causes full re-renders and DOM churn; track item.id reuses nodes.
Detach expensive, rarely-changing subtrees — with signals this is rarely needed; ChangeDetectorRef.detach() is a last resort.
ExpressionChangedAfterItHasBeenChecked: it means state changed during a CD pass — usually a child writing to a parent in ngAfterViewInit, or an effect() writing signals. Fix the data flow (compute upward with computed, emit through output()), don't wrap in setTimeout.
3. Initial load and bundle size
Lazy load every feature route (loadChildren/loadComponent); the initial bundle should contain the shell, auth, and the landing route only.
@defer for heavy components below the fold or behind interaction: @defer (on viewport) { <app-chart/> } @placeholder { <div class="skeleton"/> }. Triggers: on idle, on viewport, on interaction, on hover, on timer, when <condition>. Add @loading and @error blocks.
Bundle budgets in angular.json (initial warning ~500 kB, error ~1 MB; adjust to the app) and fail CI on breach.
Analyse with ng build --stats-json then esbuild's analyzer: look for moment.js (→ date-fns/Temporal), full lodash, RxJS operator misuse, whole icon fonts, Material modules imported at root instead of per component.
Standalone imports per component pull in only what each template uses — another reason to leave NgModules.
Fonts: preload one critical font, font-display: swap, subset. Images: NgOptimizedImage (ngSrc, priority on the LCP image, width/height, sizes) — it enforces best practices.
Preconnect to API and CDN origins in index.html.
Third-party scripts deferred and measured; they're frequently the entire INP problem.
4. SSR, hydration, and Core Web Vitals
@angular/ssr with provideClientHydration() for public pages — hydration reuses server DOM instead of re-rendering; verify no hydration mismatch warnings.
Incremental hydration (withIncrementalHydration() + @defer (hydrate on viewport)) so non-critical islands stay static until needed.
Event replay (withEventReplay()) so clicks during hydration aren't lost.
Avoid document/window access outside afterNextRender/isPlatformBrowser guards — it breaks SSR and forces client-only rendering.
Prerender static routes at build time (prerender in angular.json / getPrerenderParams).
5. Lists and tables
CDK Virtual Scroll (cdk-virtual-scroll-viewport + *cdkVirtualFor) for > ~100 rows; itemSize fixed when possible.
Server-side pagination/sorting/filtering for large datasets; never load 10k rows to filter client-side.
Row components OnPush with signal inputs; no functions in row templates.
Material Table: virtual scroll via CDK or paginate; avoid MatSort/MatPaginator on huge client arrays.
Debounce filter inputs (RxJS debounceTime(300) + distinctUntilChanged + switchMap), but update the input echo immediately.
6. RxJS-related leaks and waste
Every manual subscribe() needs takeUntilDestroyed() (or toSignal, which cleans up). Missing teardown is the #1 Angular memory leak.
shareReplay({ bufferSize: 1, refCount: true }) for shared HTTP results; without refCount the source lives forever.
switchMap for typeahead/route param changes (cancels stale requests); exhaustMap for submit.
Don't create Observables in templates or getters — each CD cycle creates a new stream and AsyncPipe resubscribes.
Router events subscriptions in many components → centralise in one service exposing signals.
7. Runtime hygiene
Heavy synchronous work (sorting, parsing, crypto) → Web Worker (ng generate web-worker).
runOutsideAngular for high-frequency events (scroll, mousemove, resize) when still on zone.js.
afterRender/afterNextRender for DOM measurement instead of setTimeout(0).
content-visibility: auto on long off-screen sections; will-change sparingly.
Avoid ViewEncapsulation.None styles with deep selectors — global style recalcs get expensive.
8. Report format
## Baseline
<metric>: <value> (production build, device, network)
## Bucket
Change detection / Bundle / Template work / Memory
## Findings (impact order)
1. <problem> — <evidence from DevTools profiler / analyzer> — est. gain
## Fixes
Minimal diff each, with before/after expectation
## Verify
Same measurement repeated
Anti-patterns to reject
Default change detection on new components.
Function calls or new Date() / JSON.stringify in templates.
@for without track, or track $index on reorderable data.
setTimeout to dodge ExpressionChangedAfterItHasBeenChecked.
subscribe() in ngOnInit with no teardown.
SharedModule imported into every lazy chunk (duplicates code into each).
pure: false pipes for formatting.
Rendering all rows of a large dataset "because virtual scroll is complicated".
---
name: angular-performance
description: Diagnose and fix Angular performance — slow change detection, ExpressionChangedAfterItHasBeenChecked, big bundles, slow initial load, janky lists/tables, memory leaks from subscriptions, and poor Core Web Vitals. Use this whenever the user says an Angular app is slow, asks about OnPush, zoneless, signals for performance, @defer, hydration, lazy loading, trackBy/track, CDK virtual scroll, bundle budgets, or profiling with Angular DevTools. Also apply it when reviewing Angular code that uses function calls or pipes with heavy logic in templates, `*ngFor` without track, default change detection, or manual subscriptions.
metadata:
technology: Angular
type: performance
---
# Angular Performance
Angular performance problems cluster into three buckets: **change detection doing too much work**, **too much JavaScript shipped**, and **templates doing work on every check**. Identify the bucket before proposing a fix.
## 1. Measure
| Symptom | Tool | Metric |
|---|---|---|
| Interactions feel slow | **Angular DevTools → Profiler** (record, see CD cycles and per-component time) | Number of change-detection runs per interaction, longest component |
| Slow load | Lighthouse, `ng build --stats-json` + `esbuild` analyzer / `source-map-explorer` | LCP, initial bundle size, number of lazy chunks |
| Jank while scrolling/typing | Chrome Performance panel | Long tasks, time in `tick()` / `refreshView` |
| Memory growth across routes | DevTools heap snapshots | Retained components, subscriptions |
| Layout shift | Lighthouse CLS | Images/fonts without dimensions, late-loaded content |
Capture a baseline with numbers before changing anything. Profile a production build (`ng build`, `ng serve --configuration production`), not dev.
## 2. Change detection — the biggest lever
Work down this list:
1. **`ChangeDetectionStrategy.OnPush` on every component.** Default CD checks the entire tree on every event; OnPush checks a component only when an input reference changes, an event fires inside it, or a signal it reads changes. Add it, then fix what breaks — breakage means something is mutating objects in place.
2. **Signals for all component state.** With signals, Angular marks only the exact components that read a changed signal. Convert `@Input()` → `input()`, fields → `signal()`, derived → `computed()`.
3. **Go zoneless.** `provideZonelessChangeDetection()` removes zone.js: no more CD on every `setTimeout`, every `fetch`, every mouse move. Requires OnPush + signals everywhere; third-party libs must not rely on zone. This is the end state for new apps.
4. **Kill template function calls.** `{{ formatPrice(item.price) }}` runs on every CD cycle. Replace with a pure pipe (memoised per input) or a `computed()`.
5. **Pure pipes over impure.** Never `pure: false` unless the pipe genuinely depends on external mutable state; use signals instead.
6. **`@for` with `track`** on a stable id. `track $index` on mutable lists causes full re-renders and DOM churn; `track item.id` reuses nodes.
7. **Detach expensive, rarely-changing subtrees** — with signals this is rarely needed; `ChangeDetectorRef.detach()` is a last resort.
`ExpressionChangedAfterItHasBeenChecked`: it means state changed *during* a CD pass — usually a child writing to a parent in `ngAfterViewInit`, or an `effect()` writing signals. Fix the data flow (compute upward with `computed`, emit through `output()`), don't wrap in `setTimeout`.
## 3. Initial load and bundle size
- **Lazy load every feature route** (`loadChildren`/`loadComponent`); the initial bundle should contain the shell, auth, and the landing route only.
- **`@defer`** for heavy components below the fold or behind interaction: `@defer (on viewport) { <app-chart/> } @placeholder { <div class="skeleton"/> }`. Triggers: `on idle`, `on viewport`, `on interaction`, `on hover`, `on timer`, `when <condition>`. Add `@loading` and `@error` blocks.
- **Bundle budgets** in `angular.json` (`initial` warning ~500 kB, error ~1 MB; adjust to the app) and fail CI on breach.
- **Analyse** with `ng build --stats-json` then esbuild's analyzer: look for moment.js (→ `date-fns`/Temporal), full lodash, RxJS operator misuse, whole icon fonts, Material modules imported at root instead of per component.
- **Standalone imports per component** pull in only what each template uses — another reason to leave NgModules.
- **Fonts**: preload one critical font, `font-display: swap`, subset. **Images**: `NgOptimizedImage` (`ngSrc`, `priority` on the LCP image, `width`/`height`, `sizes`) — it enforces best practices.
- **Preconnect** to API and CDN origins in `index.html`.
- **Third-party scripts** deferred and measured; they're frequently the entire INP problem.
## 4. SSR, hydration, and Core Web Vitals
- `@angular/ssr` with `provideClientHydration()` for public pages — hydration reuses server DOM instead of re-rendering; verify no hydration mismatch warnings.
- **Incremental hydration** (`withIncrementalHydration()` + `@defer (hydrate on viewport)`) so non-critical islands stay static until needed.
- **Event replay** (`withEventReplay()`) so clicks during hydration aren't lost.
- Avoid `document`/`window` access outside `afterNextRender`/`isPlatformBrowser` guards — it breaks SSR and forces client-only rendering.
- Prerender static routes at build time (`prerender` in `angular.json` / `getPrerenderParams`).
## 5. Lists and tables
- **CDK Virtual Scroll** (`cdk-virtual-scroll-viewport` + `*cdkVirtualFor`) for > ~100 rows; `itemSize` fixed when possible.
- Server-side pagination/sorting/filtering for large datasets; never load 10k rows to filter client-side.
- Row components `OnPush` with signal inputs; no functions in row templates.
- Material Table: virtual scroll via CDK or paginate; avoid `MatSort`/`MatPaginator` on huge client arrays.
- Debounce filter inputs (RxJS `debounceTime(300)` + `distinctUntilChanged` + `switchMap`), but update the input echo immediately.
## 6. RxJS-related leaks and waste
- Every manual `subscribe()` needs `takeUntilDestroyed()` (or `toSignal`, which cleans up). Missing teardown is the #1 Angular memory leak.
- `shareReplay({ bufferSize: 1, refCount: true })` for shared HTTP results; without `refCount` the source lives forever.
- `switchMap` for typeahead/route param changes (cancels stale requests); `exhaustMap` for submit.
- Don't create Observables in templates or getters — each CD cycle creates a new stream and `AsyncPipe` resubscribes.
- Router `events` subscriptions in many components → centralise in one service exposing signals.
## 7. Runtime hygiene
- Heavy synchronous work (sorting, parsing, crypto) → Web Worker (`ng generate web-worker`).
- `runOutsideAngular` for high-frequency events (scroll, mousemove, resize) when still on zone.js.
- `afterRender`/`afterNextRender` for DOM measurement instead of `setTimeout(0)`.
- `content-visibility: auto` on long off-screen sections; `will-change` sparingly.
- Avoid `ViewEncapsulation.None` styles with deep selectors — global style recalcs get expensive.
## 8. Report format
```
## Baseline
<metric>: <value> (production build, device, network)
## Bucket
Change detection / Bundle / Template work / Memory
## Findings (impact order)
1. <problem> — <evidence from DevTools profiler / analyzer> — est. gain
## Fixes
Minimal diff each, with before/after expectation
## Verify
Same measurement repeated
```
## Anti-patterns to reject
- Default change detection on new components.
- Function calls or `new Date()` / `JSON.stringify` in templates.
- `@for` without `track`, or `track $index` on reorderable data.
- `setTimeout` to dodge `ExpressionChangedAfterItHasBeenChecked`.
- `subscribe()` in `ngOnInit` with no teardown.
- `SharedModule` imported into every lazy chunk (duplicates code into each).
- `pure: false` pipes for formatting.
- Rendering all rows of a large dataset "because virtual scroll is complicated".