git:20260708.a07936c to git:20260905.f2e5074

4 added, 535 removed. Audit A to A.

---
name: angular-best-practices
description: "Angular performance optimization and best practices guide. Use when writing, reviewing, or refactoring Angular code for optimal performance, bundle size, and rendering efficiency."
risk: safe
source: self
date_added: "2026-02-27"
---
# Angular Best Practices
Comprehensive performance optimization guide for Angular applications. Contains prioritized rules for eliminating performance bottlenecks, optimizing bundles, and improving rendering.
+ ## Detailed Guide
+
+ Read [the detailed guide](references/detailed-guide.md) before executing this skill. It retains the complete procedure and reference material. Treat its safety, prerequisites, and validation requirements as mandatory. For focused work, load the relevant sections; for end-to-end work, read the guide completely.
+
## When to Use
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
---
- ## Rule Categories by Priority
-
- | 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
-
- ```typescript
- import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll } from '@angular/cdk/scrolling';
-
- @Component({
- imports: [CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll],
- template: `
- <cdk-virtual-scroll-viewport itemSize="50" class="viewport">
- <div *cdkVirtualFor="let item of items" class="item">
- {{ item.name }}
- </div>
- </cdk-virtual-scroll-viewport>
- `
- })
- ```
-
- ### Prefer Pure Pipes Over Methods
-
- ```typescript
- // CORRECT - Pure pipe, memoized
- @Pipe({ name: 'filterActive', standalone: true, pure: true })
- export class FilterActivePipe implements PipeTransform {
- transform(items: Item[]): Item[] {
- return items.filter(i => i.active);
- }
- }
-
- // Template
- @for (item of items() | filterActive; track item.id) { ... }
-
- // WRONG - Method called every change detection
- @for (item of getActiveItems(); track item.id) { ... }
- ```
-
- ### Use computed() for Derived Data
-
- ```typescript
- // CORRECT - Computed, cached until dependencies change
- export class ProductStore {
- products = signal<Product[]>([]);
- filter = signal('');
-
- filteredProducts = computed(() => {
- const f = this.filter().toLowerCase();
- return this.products().filter(p =>
- p.name.toLowerCase().includes(f)
- );
- });
- }
-
- // WRONG - Recalculates every access
- get filteredProducts() {
- return this.products.filter(p =>
- p.name.toLowerCase().includes(this.filter)
- );
- }
- ```
-
- ---
-
- ## 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 every render
- @for (item of items() | sort:'name'; track item.id) { ... }
- ```
-
- ---
-
- ## 7. State Management (MEDIUM)
-
- ### Use Selectors to Prevent Re-renders
-
- ```typescript
- // CORRECT - Selective subscription
- @Component({
- template: `<span>{{ userName() }}</span>`,
- })
- class HeaderComponent {
- private store = inject(Store);
- // Only re-renders when userName changes
- userName = this.store.selectSignal(selectUserName);
- }
-
- // WRONG - Subscribing to entire state
- @Component({
- template: `<span>{{ state().user.name }}</span>`,
- })
- class HeaderComponent {
- private store = inject(Store);
- // Re-renders on ANY state change
- state = toSignal(this.store);
- }
- ```
-
- ### Colocate State with Features
-
- ```typescript
- // CORRECT - Feature-scoped store
- @Injectable() // NOT providedIn: 'root'
- export class ProductStore { ... }
-
- @Component({
- providers: [ProductStore], // Scoped to component tree
- })
- export class ProductPageComponent {
- store = inject(ProductStore);
- }
-
- // WRONG - Everything in global store
- @Injectable({ providedIn: 'root' })
- export class GlobalStore {
- // Contains ALL app state - hard to tree-shake
- }
- ```
-
- ---
-
- ## 8. Memory Management (LOW-MEDIUM)
-
- ### Use takeUntilDestroyed for Subscriptions
-
- ```typescript
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-
- @Component({...})
- export class DataComponent {
- private destroyRef = inject(DestroyRef);
-
- constructor() {
- this.data$.pipe(
- takeUntilDestroyed(this.destroyRef)
- ).subscribe(data => this.process(data));
- }
- }
-
- // WRONG - Manual subscription management
- export class DataComponent implements OnDestroy {
- private subscription!: Subscription;
-
- ngOnInit() {
- this.subscription = this.data$.subscribe(...);
- }
-
- ngOnDestroy() {
- this.subscription.unsubscribe(); // Easy to forget
- }
- }
- ```
-
- ### Prefer Signals Over Subscriptions
-
- ```typescript
- // CORRECT - No subscription needed
- @Component({
- template: `<div>{{ data().name }}</div>`,
- })
- export class Component {
- data = toSignal(this.service.data$, { initialValue: null });
- }
-
- // WRONG - Manual subscription
- @Component({
- template: `<div>{{ data?.name }}</div>`,
- })
- export class Component implements OnInit, OnDestroy {
- data: Data | null = null;
- private sub!: Subscription;
-
- ngOnInit() {
- this.sub = this.service.data$.subscribe((d) => (this.data = d));
- }
-
- ngOnDestroy() {
- this.sub.unsubscribe();
- }
- }
- ```
-
- ---
-
- ## Quick Reference Checklist
-
- ### New Component
-
- - [ ] `changeDetection: ChangeDetectionStrategy.OnPush`
- - [ ] `standalone: true`
- - [ ] Signals for state (`signal()`, `input()`, `output()`)
- - [ ] `inject()` for dependencies
- - [ ] `@for` with `track` expression
-
- ### Performance Review
-
- - [ ] No methods in templates (use pipes or computed)
- - [ ] Large lists virtualized
- - [ ] Heavy components deferred
- - [ ] Routes lazy-loaded
- - [ ] Third-party libs dynamically imported
-
- ### SSR Check
-
- - [ ] Hydration configured
- - [ ] Critical content renders first
- - [ ] Non-critical content uses `@defer (hydrate on ...)`
- - [ ] TransferState for server-fetched data
-
- ---
-
- ## Resources
-
- - [Angular Performance Guide](https://angular.dev/best-practices/performance)
- - [Zoneless Angular](https://angular.dev/guide/experimental/zoneless)
- - [Angular SSR Guide](https://angular.dev/guide/ssr)
- - [Change Detection Deep Dive](https://angular.dev/guide/change-detection)
## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.