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 ngThis skill uses the workspace's default tool permissions.
**Reference**: https://angular.dev/guide/performance
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
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.