Help us improve
Share bugs, ideas, or general feedback.
From ng
Angular performance patterns — OnPush, signals, defer, lazy loading, virtual scroll. Auto-invoked when reviewing or optimizing Angular code.
npx claudepluginhub mayeedwin/angular-plugin --plugin ngHow this skill is triggered — by the user, by Claude, or both
Slash command
/ng:angular-performanceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Reference**: https://angular.dev/guide/performance
Optimizes Angular code for performance, bundle size, and rendering efficiency using Signals, OnPush, Zoneless, lazy loading, and SSR hydration patterns.
Optimizes Angular rendering using OnPush change detection, trackBy, virtual scrolling, deferrable views, and signals for zoneless-ready apps.
Provides expert Angular/TypeScript patterns for standalone components, signals, RxJS, NgRx state management, smart/dumb components, and performance.
Share bugs, ideas, or general feedback.
Reference: https://angular.dev/guide/performance
OnPush@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
With OnPush, the component only re-renders when:
@Input() reference changesasync pipe emitssignal() or computed() changesmarkForCheck() is called manually// Prefer signals for component-local state — no zone involvement
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
// Use toSignal() to bridge observables
readonly users = toSignal(this.userService.getAll(), { initialValue: [] });
@for — track is mandatory<!-- Always provide a unique track expression -->
@for (item of items(); track item.id) {
<app-item [item]="item" />
}
Poor track (avoid):
@for (item of items(); track $index) { } <!-- Forces full re-render on reorder -->
@defer for below-the-fold content<!-- Defer heavy components until visible -->
@defer (on viewport) {
<app-heavy-chart [data]="data()" />
} @placeholder {
<div class="chart-skeleton"></div>
} @loading (minimum 200ms) {
<app-spinner />
}
Use @defer triggers:
on viewport — when element enters viewporton idle — when browser is idleon interaction — on click/focuswhen condition — when expression is trueon timer(2s) — after delay<!-- Bad — called on every change detection cycle -->
<span>{{ formatDate(item.date) }}</span>
<!-- Good — computed once -->
<!-- In component: readonly formattedDate = computed(() => formatDate(this.item()?.date)); -->
<span>{{ formattedDate() }}</span>
Use NgOptimizedImage for all <img> tags:
imports: [NgOptimizedImage]
<!-- Automatic: lazy loading, size hints, LCP priority -->
<img ngSrc="/assets/hero.jpg" width="800" height="400" priority />
<img ngSrc="/assets/product.jpg" width="200" height="200" />
{
path: 'products',
loadChildren: () => import('@pages/products/products.routes').then(m => m.PRODUCTS_ROUTES),
}
// or for single component:
{
path: 'about',
loadComponent: () => import('@pages/about/about.component').then(m => m.AboutComponent),
}
provideRouter(routes, withPreloading(PreloadAllModules))
// or custom: withPreloading(QuicklinkStrategy) from ngx-quicklink
For lists with more than ~50 items:
imports: [ScrollingModule] // from @angular/cdk/scrolling
<cdk-virtual-scroll-viewport itemSize="72" class="list-viewport">
@for (item of items; track item.id) {
<app-list-item *cdkVirtualFor="let item of items" [item]="item" />
}
</cdk-virtual-scroll-viewport>
Flag these patterns as potential bundle bloat:
lodash-es and named importsdate-fns or dayjsprovideAnimationsAsync() (defers animations module)Check bundle budgets in angular.json:
"budgets": [
{ "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" },
{ "type": "anyComponentStyle", "maximumWarning": "4kb" }
]
For public-facing apps, recommend:
provideClientHydration(withEventReplay())
For pages that don't need SSR hydration, use render mode:
// app.routes.server.ts
export const serverRouteConfig: ServerRoute[] = [
{ path: '/admin/**', mode: RenderMode.Client },
{ path: '/**', mode: RenderMode.Prerender },
];
For maximum performance with signals-based apps:
// app.config.ts
provideExperimentalZonelessChangeDetection()
Remove zone.js from polyfills in angular.json after enabling.