angular-best-practices
kumaran-is/claude-code-onboarding/.claude/skills/angular-best-practices/SKILL.md
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.
Skill35 starsChanged 4 months ago
What's in it
- Iron Law
- When to Apply
- Priority Matrix
- 1. Change Detection (CRITICAL)
- Use OnPush Change Detection
- Prefer Signals Over Mutable Properties
- Enable Zoneless for New Projects
- 2. Async Operations & Waterfalls (CRITICAL)
- Eliminate Sequential Data Fetching
- Avoid Client-Side Waterfalls in SSR
- 3. Bundle Optimization (CRITICAL)
- Lazy Load Routes
- Use @defer for Heavy Components
- Avoid Barrel File Re-exports
- Dynamic Import Third-Party Libraries
- 4. Rendering Performance (HIGH)
- Always Use trackBy with @for
- Use Virtual Scrolling for Large Lists
- Prefer Pure Pipes Over Methods
- Use computed() for Derived Data
- 5. Server-Side Rendering (HIGH)
- Configure Incremental Hydration
- Defer Non-Critical Content
- Use TransferState for SSR Data
- 6. Template Optimization (MEDIUM)
- Use New Control Flow Syntax
- Avoid Complex Template Expressions
- 7. State Management (MEDIUM)
- 8. Memory Management (LOW-MEDIUM)
- Quick Reference Checklists
Tools it asks for
- Read
- Edit
- Write
- Glob
- Grep
- Bash
- WebFetch
- mcp__context7__resolve-library-id
- mcp__context7__query-docs
- mcp__angular-cli__get_best_practices
---
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
```typescript
// ✅ CORRECT - OnPush with Signals
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{ count() }}</div>`,
})
export class CounterComponent {
count = signal(0);
}
// ❌ WRONG - Default change detection
@Component({
template: `<div>{{ count }}</div>`, // Checked every cycle
})
export class CounterComponent {
count = 0;
}
```
### Prefer Signals Over Mutable Properties
```typescript
// ✅ CORRECT - Signals trigger precise updates
@Component({
template: `
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
`,
})
export class DashboardComponent {
title = signal("Dashboard");
count = signal(0);
}
// ❌ WRONG - Mutable properties require zone.js checks
@Component({
template: `
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
`,
})
export class DashboardComponent {
title = "Dashboard";
count = 0;
}
```
### Enable Zoneless for New Projects
```typescript
// main.ts - Zoneless Angular (v20+)
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
```typescript
// ❌ WRONG - Nested subscriptions create waterfalls
this.route.params.subscribe((params) => {
// 1. Wait for params
this.userService.getUser(params.id).subscribe((user) => {
// 2. Wait for user
this.postsService.getPosts(user.id).subscribe((posts) => {
// 3. Wait for posts
});
});
});
// ✅ CORRECT - Parallel execution with forkJoin
forkJoin({
user: this.userService.getUser(id),
posts: this.postsService.getPosts(id),
}).subscribe((data) => {
// Fetched in parallel
});
// ✅ CORRECT - Flatten dependent calls with switchMap
this.route.params
.pipe(
map((p) => p.id),
switchMap((id) => this.userService.getUser(id)),
)
.subscribe();
```
### Avoid Client-Side Waterfalls in SSR
```typescript
// ✅ CORRECT - Use resolvers or blocking hydration for critical data
export const route: Route = {
path: "profile/:id",
resolve: { data: profileResolver }, // Fetched on server before navigation
component: ProfileComponent,
};
// ❌ WRONG - Component fetches data on init
class ProfileComponent implements OnInit {
ngOnInit() {
// Starts ONLY after JS loads and component renders
this.http.get("/api/profile").subscribe();
}
}
```
---
## 3. Bundle Optimization (CRITICAL)
### Lazy Load Routes
```typescript
// ✅ CORRECT - Lazy load feature 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,
),
},
];
// ❌ WRONG - Eager loading everything
import { AdminModule } from "./admin/admin.module";
export const routes: Routes = [
{ path: "admin", component: AdminComponent }, // In main bundle
];
```
### Use @defer for Heavy Components
```html
<!-- ✅ CORRECT - Heavy component loads on demand -->
@defer (on viewport) {
<app-analytics-chart [data]="data()" />
} @placeholder {
<div class="chart-skeleton"></div>
}
<!-- ❌ WRONG - Heavy component in initial bundle -->
<app-analytics-chart [data]="data()" />
```
### Avoid Barrel File Re-exports
```typescript
// ❌ WRONG - Imports entire barrel, breaks tree-shaking
import { Button, Modal, Table } from "@shared/components";
// ✅ CORRECT - Direct imports
import { Button } from "@shared/components/button/button.component";
import { Modal } from "@shared/components/modal/modal.component";
```
### Dynamic Import Third-Party Libraries
```typescript
// ✅ CORRECT - Load heavy library on demand
async loadChart() {
const { Chart } = await import('chart.js');
this.chart = new Chart(this.canvas, config);
}
// ❌ WRONG - Bundle Chart.js in main chunk
import { Chart } from 'chart.js';
```
---
## 4. Rendering Performance (HIGH)
### Always Use trackBy with @for
```html
<!-- ✅ CORRECT - Efficient DOM updates -->
@for (item of items(); track item.id) {
<app-item-card [item]="item" />
}
<!-- ❌ WRONG - Entire list re-renders on any change -->
@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](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](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](references/extended-patterns.md) for before/after examples.
---
## 5. Server-Side Rendering (HIGH)
### Configure Incremental Hydration
```typescript
// app.config.ts
import {
provideClientHydration,
withIncrementalHydration,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration(), withEventReplay()),
],
};
```
### Defer Non-Critical Content
```html
<!-- Critical above-the-fold content -->
<app-header />
<app-hero />
<!-- Below-fold deferred with hydration triggers -->
@defer (hydrate on viewport) {
<app-product-grid />
} @defer (hydrate on interaction) {
<app-chat-widget />
}
```
### Use TransferState for SSR Data
```typescript
@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(this.platformId)) {
this.transferState.set(stateKey, data);
}
}),
);
}
}
```
---
## 6. Template Optimization (MEDIUM)
### Use New Control Flow Syntax
```html
<!-- ✅ CORRECT - New control flow (faster, smaller bundle) -->
@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>
}
<!-- ❌ WRONG - Legacy structural directives -->
<span *ngIf="user; else guest">{{ user.name }}</span>
<ng-template #guest><span>Guest</span></ng-template>
```
### Avoid Complex Template Expressions
```typescript
// ✅ CORRECT - Precompute in component
class Component {
items = signal<Item[]>([]);
sortedItems = computed(() =>
[...this.items()].sort((a, b) => a.name.localeCompare(b.name))
);
}
// Template
@for (item of sortedItems(); track item.id) { ... }
// ❌ WRONG - Sorting in template on every render
@for (item of items() | sort:'name'; track item.id) { ... }
```
---
## 7. State Management (MEDIUM)
> See [references/extended-patterns.md](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](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](references/extended-patterns.md) for the full New Component, Performance Review, and SSR checklists.
**New component minimum:**
- [ ] `changeDetection: ChangeDetectionStrategy.OnPush`
- [ ] `standalone: true`
- [ ] `@for` with `track item.id` (not `$index`)
---
## 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
More agent context in kumaran-is/claude-code-onboarding
157 other files this repository gives its agents, the first 60 shown.
CLAUDE.md
Skill
- a2ui-angular.claude/skills/a2ui-angular/SKILL.md
- accessibility-audit.claude/skills/accessibility-audit/SKILL.md
- adk-deploy-guide.claude/skills/adk-deploy-guide/SKILL.md
- adk-dev-guide.claude/skills/adk-dev-guide/SKILL.md
- adk-eval-guide.claude/skills/adk-eval-guide/SKILL.md
- adk-observability-guide.claude/skills/adk-observability-guide/SKILL.md
- agentic-ai-coding-standard.claude/skills/agentic-ai-coding-standard/SKILL.md
- agentic-ai-dev.claude/skills/agentic-ai-dev/SKILL.md
- ai-audit.claude/skills/ai-audit/SKILL.md
- ai-chat.claude/skills/ai-chat/SKILL.md
- ai-decision-record.claude/skills/ai-decision-record/SKILL.md
- ai-incident-response.claude/skills/ai-incident-response/SKILL.md
- ai-launch-check.claude/skills/ai-launch-check/SKILL.md
- ai-playbook.claude/skills/ai-playbook/SKILL.md
- angular.claude/skills/angular/SKILL.md
- angular-spa.claude/skills/angular-spa/SKILL.md
- angular-ui-patterns.claude/skills/angular-ui-patterns/SKILL.md
- api-design-principles.claude/skills/api-design-principles/SKILL.md
- app-store-optimization.claude/skills/app-store-optimization/SKILL.md
- architect-review.claude/skills/architect-review/SKILL.md
- architecture-decision-records.claude/skills/architecture-decision-records/SKILL.md
- architecture-design.claude/skills/architecture-design/SKILL.md
- asc-cli-usage.claude/skills/asc-cli-usage/SKILL.md
- asc-crash-triage.claude/skills/asc-crash-triage/SKILL.md
- asc-id-resolver.claude/skills/asc-id-resolver/SKILL.md
- asc-release-flow.claude/skills/asc-release-flow/SKILL.md
- asc-signing-setup.claude/skills/asc-signing-setup/SKILL.md
- asc-submission-health.claude/skills/asc-submission-health/SKILL.md
- asc-testflight-orchestration.claude/skills/asc-testflight-orchestration/SKILL.md
- browser-testing.claude/skills/browser-testing/SKILL.md
- changelog-generator.claude/skills/changelog-generator/SKILL.md
- claude-actions-auditor.claude/skills/claude-actions-auditor/SKILL.md
- clean-code.claude/skills/clean-code/SKILL.md
- codebase-onboarding.claude/skills/codebase-onboarding/SKILL.md
- code-explainer.claude/skills/code-explainer/SKILL.md
- code-reviewer.claude/skills/code-reviewer/SKILL.md
- code-simplifier.claude/skills/code-simplifier/SKILL.md
- comment-analyzer.claude/skills/comment-analyzer/SKILL.md
- database-schema-designer.claude/skills/database-schema-designer/SKILL.md
- ddd-architect.claude/skills/ddd-architect/SKILL.md
- decision-frameworks.claude/skills/decision-frameworks/SKILL.md
- dedup-code-agent.claude/skills/dedup-code-agent/SKILL.md
- deployment-ci-cd.claude/skills/deployment-ci-cd/SKILL.md
- design-system.claude/skills/design-system/SKILL.md
- docker.claude/skills/docker/SKILL.md
- documentation-generation.claude/skills/documentation-generation/SKILL.md
- domain-finder.claude/skills/domain-finder/SKILL.md
- error-detective.claude/skills/error-detective/SKILL.md
- eval-guide.claude/skills/eval-guide/SKILL.md
- feature-forge.claude/skills/feature-forge/SKILL.md
- firebase-basics.claude/skills/firebase-basics/SKILL.md
- firebase-hosting-basics.claude/skills/firebase-hosting-basics/SKILL.md
- fixing-accessibility.claude/skills/fixing-accessibility/SKILL.md
- fixing-motion-performance.claude/skills/fixing-motion-performance/SKILL.md
- flutter-animations.claude/skills/flutter-animations/SKILL.md
- flutter-genui.claude/skills/flutter-genui/SKILL.md
- flutter-mobile.claude/skills/flutter-mobile/SKILL.md
- flutter-security-expert.claude/skills/flutter-security-expert/SKILL.md
- frontend-design.claude/skills/frontend-design/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

