| name | angular-best-practices |
| description | Impact-prioritized Angular 21.x best practices — CRITICAL: OnPush+Signals, async waterfalls, bundle optimization. HIGH: rendering performance, SSR hydration. MEDIUM: template optimization, state management. LOW: memory management. Load when reviewing Angular code, writing components, or optimizing performance. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch, mcp__context7__resolve-library-id, mcp__context7__query-docs, mcp__angular-cli__get_best_practices |
| metadata | {"triggers":"Angular performance, Angular optimization, Angular OnPush, Angular bundle, Angular lazy loading, Angular SSR, Angular memory leak, Angular trackBy, Angular virtual scroll, Angular review","related-skills":"angular-spa, angular, angular-ui-patterns","domain":"frontend","role":"specialist","scope":"review","output-format":"checklist"} |
| last-reviewed | 2026-03-15 |
Iron Law
LOAD angular-spa FIRST for TailwindCSS 4.x + daisyUI workspace conventions. This skill provides ranked best practices for review and optimization — not implementation templates.
When to Apply
Reference these guidelines when:
- Writing new Angular components or pages
- Implementing data fetching patterns
- Reviewing code for performance issues
- Refactoring existing Angular code
- Optimizing bundle size or load times
- Configuring SSR/hydration
Priority Matrix
| Priority | Category | Impact | Focus |
|---|
| 1 | Change Detection | CRITICAL | Signals, OnPush, Zoneless |
| 2 | Async Waterfalls | CRITICAL | RxJS patterns, SSR preloading |
| 3 | Bundle Optimization | CRITICAL | Lazy loading, tree shaking |
| 4 | Rendering Performance | HIGH | @defer, trackBy, virtualization |
| 5 | Server-Side Rendering | HIGH | Hydration, prerendering |
| 6 | Template Optimization | MEDIUM | Control flow, pipes |
| 7 | State Management | MEDIUM | Signal patterns, selectors |
| 8 | Memory Management | LOW-MEDIUM | Cleanup, subscriptions |
1. Change Detection (CRITICAL)
Use OnPush Change Detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{ count() }}</div>`,
})
export class CounterComponent {
count = signal(0);
}
@Component({
template: `<div>{{ count }}</div>`,
})
export class CounterComponent {
count = 0;
}
Prefer Signals Over Mutable Properties
@Component({
template: `
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
`,
})
export class DashboardComponent {
title = signal("Dashboard");
count = signal(0);
}
@Component({
template: `
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
`,
})
export class DashboardComponent {
title = "Dashboard";
count = 0;
}
Enable Zoneless for New Projects
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
Benefits:
- No zone.js patches on async APIs
- Smaller bundle (~15KB savings)
- Clean stack traces for debugging
- Better micro-frontend compatibility
2. Async Operations & Waterfalls (CRITICAL)
Eliminate Sequential Data Fetching
this.route.params.subscribe((params) => {
this.userService.getUser(params.id).subscribe((user) => {
this.postsService.getPosts(user.id).subscribe((posts) => {
});
});
});
forkJoin({
user: this.userService.getUser(id),
posts: this.postsService.getPosts(id),
}).subscribe((data) => {
});
this.route.params
.pipe(
map((p) => p.id),
switchMap((id) => ..(id)),
)
.();
Avoid Client-Side Waterfalls in SSR
export const route: Route = {
path: "profile/:id",
resolve: { data: profileResolver },
component: ProfileComponent,
};
class ProfileComponent implements OnInit {
ngOnInit() {
this.http.get("/api/profile").subscribe();
}
}
3. Bundle Optimization (CRITICAL)
Lazy Load Routes
export const routes: Routes = [
{
path: "admin",
loadChildren: () =>
import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
{
path: "dashboard",
loadComponent: () =>
import("./dashboard/dashboard.component").then(
(m) => m.DashboardComponent,
),
},
];
import { AdminModule } from "./admin/admin.module";
export const routes: Routes = [
{ path: "admin", component: AdminComponent },
];
Use @defer for Heavy Components
@defer (on viewport) {
<app-analytics-chart [data]="data()" />
} @placeholder {
<div class="chart-skeleton"></div>
}
<app-analytics-chart [data]="data()" />
Avoid Barrel File Re-exports
import { Button, Modal, Table } from "@shared/components";
import { Button } from "@shared/components/button/button.component";
import { Modal } from "@shared/components/modal/modal.component";
Dynamic Import Third-Party Libraries
async loadChart() {
const { Chart } = await import('chart.js');
this.chart = new Chart(this.canvas, config);
}
import { Chart } from 'chart.js';
4. Rendering Performance (HIGH)
Always Use trackBy with @for
@for (item of items(); track item.id) {
<app-item-card [item]="item" />
}
@for (item of items(); track $index) {
<app-item-card [item]="item" />
}
Use Virtual Scrolling for Large Lists
Use CdkVirtualScrollViewport with itemSize for lists >50 items. See references/extended-patterns.md for the full example.
Prefer Pure Pipes Over Methods
Use @Pipe({ pure: true }) — memoized and called only when inputs change. Never call methods in templates — they run every change detection cycle. See references/extended-patterns.md for examples.
Use computed() for Derived Data
Use computed() instead of getters — cached until signal dependencies change. See references/extended-patterns.md for before/after examples.
5. Server-Side Rendering (HIGH)
Configure Incremental Hydration
import {
provideClientHydration,
withIncrementalHydration,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration(), withEventReplay()),
],
};
Defer Non-Critical Content
<app-header />
<app-hero />
@defer (hydrate on viewport) {
<app-product-grid />
} @defer (hydrate on interaction) {
<app-chat-widget />
}
Use TransferState for SSR Data
@Injectable({ providedIn: "root" })
export class DataService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private platformId = inject(PLATFORM_ID);
getData(key: string): Observable<Data> {
const stateKey = makeStateKey<Data>(key);
if (isPlatformBrowser(this.platformId)) {
const cached = this.transferState.get(stateKey, null);
if (cached) {
this.transferState.remove(stateKey);
return of(cached);
}
}
return this.http.get<Data>(`/api/${key}`).pipe(
tap((data) => {
if (isPlatformServer(.)) {
..(stateKey, data);
}
}),
);
}
}
6. Template Optimization (MEDIUM)
Use New Control Flow Syntax
@if (user()) {
<span>{{ user()!.name }}</span>
} @else {
<span>Guest</span>
} @for (item of items(); track item.id) {
<app-item [item]="item" />
} @empty {
<p>No items</p>
}
<span *ngIf="user; else guest">{{ user.name }}</span>
<ng-template #guest><span>Guest</span></ng-template>
Avoid Complex Template Expressions
class Component {
items = signal<Item[]>([]);
sortedItems = computed(() =>
[...this.items()].sort((a, b) => a.name.localeCompare(b.name))
);
}
@for (item of sortedItems(); track item.id) { ... }
@for (item of items() | sort:'name'; track item.id) { ... }
7. State Management (MEDIUM)
See references/extended-patterns.md for full examples (selectors, feature-scoped stores).
Key rules:
- Use
store.selectSignal(selector) — subscribes only to the slice that changed, not entire state
@Injectable() without providedIn: 'root' for feature-scoped stores provided at component level
8. Memory Management (LOW-MEDIUM)
See references/extended-patterns.md for full examples (takeUntilDestroyed, toSignal).
Key rules:
- Use
takeUntilDestroyed(this.destroyRef) — auto-unsubscribes on component destroy, no manual ngOnDestroy
- Prefer
toSignal(obs$) over manual subscribe/unsubscribe entirely
Quick Reference Checklists
See references/extended-patterns.md for the full New Component, Performance Review, and SSR checklists.
New component minimum:
Related Skills
angular-spa — workspace skill: TailwindCSS 4.x, daisyUI 5.5.5, conventions
angular — core API reference: signals, standalone, routing patterns
angular-ui-patterns — UI state patterns: loading, error, empty states