git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/angular-performance-patterns" ~/.claude/skills/intense-visions-harness-engineering-angular-performance-patterns-c2663f && rm -rf "$T"
agents/skills/codex/angular-performance-patterns/SKILL.mdAngular Performance Patterns
Optimize Angular rendering with OnPush change detection, trackBy, virtual scrolling, deferrable views, and signals for zoneless-ready apps
When to Use
- A list or table is causing visible jank during scrolling or filtering
- The Angular DevTools profiler shows excessive change detection cycles
- A component tree is deep and updates are propagating to many unrelated components
- Rendering thousands of items in a
causes memory or scroll performance issues*ngFor - Heavy components below the fold are delaying time-to-interactive
Instructions
- Set
on every component. WithchangeDetection: ChangeDetectionStrategy.OnPush
, Angular only checks a component when its input references change, anOnPush
pipe emits, or a signal updates — not on every browser event.async - Use
withtrackBy
to prevent Angular from destroying and re-creating DOM nodes when the array reference changes:*ngFor
. The track function should return a stable unique identifier (e.g., the item's ID).*ngFor="let item of items; trackBy: trackById" - Use
@angular/cdk/scrolling
for lists with more than ~100 items. Virtual scrolling renders only the visible items, keeping DOM size constant regardless of data size.CdkVirtualScrollViewport - Use
for components below the fold — they won't load until the user scrolls to them, reducing initial bundle execution time.@defer (on viewport) - Move expensive pure calculations into
signals or pure pipes — both memoize their results and only recompute when dependencies change.computed() - Avoid function calls in templates (
) — they execute on every change detection cycle. Replace with{{ computeTotal() }}
signals orcomputed()
derived values.@Input() - Avoid
/setTimeout
without wrapping insetInterval
for non-UI timers — they trigger change detection on every tick.NgZone.runOutsideAngular() - Adopt signals for local component state to prepare for zoneless change detection (
in Angular 18+).provideExperimentalZonelessChangeDetection()
// OnPush + trackBy @Component({ selector: 'app-product-list', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <app-product-card *ngFor="let product of products(); trackBy: trackById" [product]="product" /> `, }) export class ProductListComponent { products = input.required<Product[]>(); trackById = (_: number, item: Product) => item.id; }
// Virtual scrolling with CDK import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; @Component({ imports: [ScrollingModule], template: ` <cdk-virtual-scroll-viewport itemSize="72" style="height: 600px"> <div *cdkVirtualFor="let item of items; trackBy: trackById" class="list-item" style="height: 72px" > {{ item.name }} </div> </cdk-virtual-scroll-viewport> `, }) export class VirtualListComponent { items = input.required<Item[]>(); trackById = (_: number, i: Item) => i.id; }
// Computed signal instead of template method call @Component({ template: `<p>Total: {{ formattedTotal() }}</p>`, }) export class CartComponent { items = signal<CartItem[]>([]); // Memoized — only recomputes when items() changes total = computed(() => this.items().reduce((s, i) => s + i.price * i.qty, 0)); formattedTotal = computed(() => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(this.total()) ); }
// Running non-UI work outside Angular zone @Injectable({ providedIn: 'root' }) export class PollingService { private ngZone = inject(NgZone); startPolling(callback: () => void, intervalMs: number): () => void { let id: ReturnType<typeof setInterval>; this.ngZone.runOutsideAngular(() => { id = setInterval(() => { // Run callback back inside zone to trigger CD if needed this.ngZone.run(callback); }, intervalMs); }); return () => clearInterval(id); } }
Details
Change detection cost model: In the default strategy, Angular traverses the entire component tree on every browser event (click, input, scroll, setTimeout, XHR). With
OnPush, Angular marks a component as "dirty" only when:
- An
reference changes (new object/array reference)@Input() - A signal read inside the component template emits
- An observable bound with
pipe emitsasync
is called explicitlyChangeDetectorRef.markForCheck()
mechanics: Without trackBy
trackBy, Angular compares list items by identity. When the array reference changes (even with the same data), Angular destroys and recreates all DOM nodes — re-triggering child lifecycle hooks. trackBy returns a key; if the key matches an existing node, Angular reuses the DOM element and only updates the changed properties.
Virtual scrolling sizing:
CdkVirtualScrollViewport requires itemSize (in pixels) for fixed-height items. For variable-height items, use AutoSizeVirtualScrollStrategy from CDK (experimental). The viewport must have an explicit height for scrolling to work.
use cases:NgZone.runOutsideAngular
- WebSocket message handlers that update a signal
loops for canvas renderingrequestAnimationFrame
for polling when only some callbacks need UI updatessetInterval
Bundle performance:
@defer creates a separate chunk for the deferred component. Use ng build --stats-json && npx webpack-bundle-analyzer dist/stats.json to verify chunk sizes. Set bundleBudgets in angular.json to fail the build if chunks exceed defined thresholds.
Profiling with Angular DevTools: Install the Angular DevTools Chrome extension. In the "Profiler" tab, record a change detection cycle and inspect which components checked and how long each took. Components with unnecessary check counts are candidates for
OnPush or signal migration.
Memoization with pure pipes: A
pure: true pipe (default) is essentially a memoized function — Angular caches the result for the same input references. For expensive formatting applied in a large *ngFor, a pure pipe avoids recomputing the format on every CD cycle.
Source
https://angular.dev/guide/best-practices/runtime-performance
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.