{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/angular-best-practices",
  "version": "1.0.0",
  "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.",
  "system_prompt_fragment": "# Angular Best Practices\n\nComprehensive performance optimization guide for Angular applications. Contains prioritized rules for eliminating performance bottlenecks, optimizing bundles, and improving rendering.\n\n## When to Apply\n\nReference these guidelines when:\n\n- Writing new Angular components or pages\n- Implementing data fetching patterns\n- Reviewing code for performance issues\n- Refactoring existing Angular code\n- Optimizing bundle size or load times\n- Configuring SSR/hydration\n\n---\n\n## Rule Categories by Priority\n\n| Priority | Category              | Impact     | Focus                           |\n| -------- | --------------------- | ---------- | ------------------------------- |\n| 1        | Change Detection      | CRITICAL   | Signals, OnPush, Zoneless       |\n| 2        | Async Waterfalls      | CRITICAL   | RxJS patterns, SSR preloading   |\n| 3        | Bundle Optimization   | CRITICAL   | Lazy loading, tree shaking      |\n| 4        | Rendering Performance | HIGH       | @defer, trackBy, virtualization |\n| 5        | Server-Side Rendering | HIGH       | Hydration, prerendering         |\n| 6        | Template Optimization | MEDIUM     | Control flow, pipes             |\n| 7        | State Management      | MEDIUM     | Signal patterns, selectors      |\n| 8        | Memory Management     | LOW-MEDIUM | Cleanup, subscriptions          |\n\n---\n\n## 1. Change Detection (CRITICAL)\n\n### Use OnPush Change Detection\n\n```typescript\n// CORRECT - OnPush with Signals\n@Component({\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  template: `<div>{{ count() }}</div>`,\n})\nexport class CounterComponent {\n  count = signal(0);\n}\n\n// WRONG - Default change detection\n@Component({\n  template: `<div>{{ count }}</div>`, // Checked every cycle\n})\nexport class CounterComponent {\n  count = 0;\n}\n```\n\n### Prefer Signals Over Mutable Properties\n\n```typescript\n// CORRECT - Signals trigger precise updates\n@Component({\n  template: `\n    <h1>{{ title() }}</h1>\n    <p>Count: {{ count() }}</p>\n  `,\n})\nexport class DashboardComponent {\n  title = signal(\"Dashboard\");\n  count = signal(0);\n}\n\n// WRONG - Mutable properties require zone.js checks\n@Component({\n  template: `\n    <h1>{{ title }}</h1>\n    <p>Count: {{ count }}</p>\n  `,\n})\nexport class DashboardComponent {\n  title = \"Dashboard\";\n  count = 0;\n}\n```\n\n### Enable Zoneless for New Projects\n\n```typescript\n// main.ts - Zoneless Angular (v20+)\nbootstrapApplication(AppComponent, {\n  providers: [provideZonelessChangeDetection()],\n});\n```\n\n**Benefits:**\n\n- No zone.js patches on async APIs\n- Smaller bundle (~15KB savings)\n- Clean stack traces for debugging\n- Better micro-frontend compatibility\n\n---\n\n## 2. Async Operations & Waterfalls (CRITICAL)\n\n### Eliminate Sequential Data Fetching\n\n```typescript\n// WRONG - Nested subscriptions create waterfalls\nthis.route.params.subscribe((params) => {\n  // 1. Wait for params\n  this.userService.getUser(params.id).subscribe((user) => {\n    // 2. Wait for user\n    this.postsService.getPosts(user.id).subscribe((posts) => {\n      // 3. Wait for posts\n    });\n  });\n});\n\n// CORRECT - Parallel execution with forkJoin\nforkJoin({\n  user: this.userService.getUser(id),\n  posts: this.postsService.getPosts(id),\n}).subscribe((data) => {\n  // Fetched in parallel\n});\n\n// CORRECT - Flatten dependent calls with switchMap\nthis.route.params\n  .pipe(\n    map((p) => p.id),\n    switchMap((id) => this.userService.getUser(id)),\n  )\n  .subscribe();\n```\n\n### Avoid Client-Side Waterfalls in SSR\n\n```typescript\n// CORRECT - Use resolvers or blocking hydration for critical data\nexport const route: Route = {\n  path: \"profile/:id\",\n  resolve: { data: profileResolver }, // Fetched on server before navigation\n  component: ProfileComponent,\n};\n\n// WRONG - Component fetches data on init\nclass ProfileComponent implements OnInit {\n  ngOnInit() {\n    // Starts ONLY after JS loads and component renders\n    this.http.get(\"/api/profile\").subscribe();\n  }\n}\n```\n\n---\n\n## 3. Bundle Optimization (CRITICAL)\n\n### Lazy Load Routes\n\n```typescript\n// CORRECT - Lazy load feature routes\nexport const routes: Routes = [\n  {\n    path: \"admin\",\n    loadChildren: () =>\n      import(\"./admin/admin.routes\").then((m) => m.ADMIN_ROUTES),\n  },\n  {\n    path: \"dashboard\",\n    loadComponent: () =>\n      import(\"./dashboard/dashboard.component\").then(\n        (m) => m.DashboardComponent,\n      ),\n  },\n];\n\n// WRONG - Eager loading everything\nimport { AdminModule } from \"./admin/admin.module\";\nexport const routes: Routes = [\n  { path: \"admin\", component: AdminComponent }, // In main bundle\n];\n```\n\n### Use @defer for Heavy Components\n\n```html\n<!-- CORRECT - Heavy component loads on demand -->\n@defer (on viewport) {\n<app-analytics-chart [data]=\"data()\" />\n} @placeholder {\n<div class=\"chart-skeleton\"></div>\n}\n\n<!-- WRONG - Heavy component in initial bundle -->\n<app-analytics-chart [data]=\"data()\" />\n```\n\n### Avoid Barrel File Re-exports\n\n```typescript\n// WRONG - Imports entire barrel, breaks tree-shaking\nimport { Button, Modal, Table } from \"@shared/components\";\n\n// CORRECT - Direct imports\nimport { Button } from \"@shared/components/button/button.component\";\nimport { Modal } from \"@shared/components/modal/modal.component\";\n```\n\n### Dynamic Import Third-Party Libraries\n\n```typescript\n// CORRECT - Load heavy library on demand\nasync loadChart() {\n  const { Chart } = await import('chart.js');\n  this.chart = new Chart(this.canvas, config);\n}\n\n// WRONG - Bundle Chart.js in main chunk\nimport { Chart } from 'chart.js';\n```\n\n---\n\n## 4. Rendering Performance (HIGH)\n\n### Always Use trackBy with @for\n\n```html\n<!-- CORRECT - Efficient DOM updates -->\n@for (item of items(); track item.id) {\n<app-item-card [item]=\"item\" />\n}\n\n<!-- WRONG - Entire list re-renders on any change -->\n@for (item of items(); track $index) {\n<app-item-card [item]=\"item\" />\n}\n```\n\n### Use Virtual Scrolling for Large Lists\n\n```typescript\nimport { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll } from '@angular/cdk/scrolling';\n\n@Component({\n  imports: [CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll],\n  template: `\n    <cdk-virtual-scroll-viewport itemSize=\"50\" class=\"viewport\">\n      <div *cdkVirtualFor=\"let item of items\" class=\"item\">\n        {{ item.name }}\n      </div>\n    </cdk-virtual-scroll-viewport>\n  `\n})\n```\n\n### Prefer Pure Pipes Over Methods\n\n```typescript\n// CORRECT - Pure pipe, memoized\n@Pipe({ name: 'filterActive', standalone: true, pure: true })\nexport class FilterActivePipe implements PipeTransform {\n  transform(items: Item[]): Item[] {\n    return items.filter(i => i.active);\n  }\n}\n\n// Template\n@for (item of items() | filterActive; track item.id) { ... }\n\n// WRONG - Method called every change detection\n@for (item of getActiveItems(); track item.id) { ... }\n```\n\n### Use computed() for Derived Data\n\n```typescript\n// CORRECT - Computed, cached until dependencies change\nexport class ProductStore {\n  products = signal<Product[]>([]);\n  filter = signal('');\n\n  filteredProducts = computed(() => {\n    const f = this.filter().toLowerCase();\n    return this.products().filter(p =>\n      p.name.toLowerCase().includes(f)\n    );\n  });\n}\n\n// WRONG - Recalculates every access\nget filteredProducts() {\n  return this.products.filter(p =>\n    p.name.toLowerCase().includes(this.filter)\n  );\n}\n```\n\n---\n\n## 5. Server-Side Rendering (HIGH)\n\n### Configure Incremental Hydration\n\n```typescript\n// app.config.ts\nimport {\n  provideClientHydration,\n  withIncrementalHydration,\n} from \"@angular/platform-browser\";\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(withIncrementalHydration(), withEventReplay()),\n  ],\n};\n```\n\n### Defer Non-Critical Content\n\n```html\n<!-- Critical above-the-fold content -->\n<app-header />\n<app-hero />\n\n<!-- Below-fold deferred with hydration triggers -->\n@defer (hydrate on viewport) {\n<app-product-grid />\n} @defer (hydrate on interaction) {\n<app-chat-widget />\n}\n```\n\n### Use TransferState for SSR Data\n\n```typescript\n@Injectable({ providedIn: \"root\" })\nexport class DataService {\n  private http = inject(HttpClient);\n  private transferState = inject(TransferState);\n  private platformId = inject(PLATFORM_ID);\n\n  getData(key: string): Observable<Data> {\n    const stateKey = makeStateKey<Data>(key);\n\n    if (isPlatformBrowser(this.platformId)) {\n      const cached = this.transferState.get(stateKey, null);\n      if (cached) {\n        this.transferState.remove(stateKey);\n        return of(cached);\n      }\n    }\n\n    return this.http.get<Data>(`/api/${key}`).pipe(\n      tap((data) => {\n        if (isPlatformServer(this.platformId)) {\n          this.transferState.set(stateKey, data);\n        }\n      }),\n    );\n  }\n}\n```\n\n---\n\n## 6. Template Optimization (MEDIUM)\n\n### Use New Control Flow Syntax\n\n```html\n<!-- CORRECT - New control flow (faster, smaller bundle) -->\n@if (user()) {\n<span>{{ user()!.name }}</span>\n} @else {\n<span>Guest</span>\n} @for (item of items(); track item.id) {\n<app-item [item]=\"item\" />\n} @empty {\n<p>No items</p>\n}\n\n<!-- WRONG - Legacy structural directives -->\n<span *ngIf=\"user; else guest\">{{ user.name }}</span>\n<ng-template #guest><span>Guest</span></ng-template>\n```\n\n### Avoid Complex Template Expressions\n\n```typescript\n// CORRECT - Precompute in component\nclass Component {\n  items = signal<Item[]>([]);\n  sortedItems = computed(() =>\n    [...this.items()].sort((a, b) => a.name.localeCompare(b.name))\n  );\n}\n\n// Template\n@for (item of sortedItems(); track item.id) { ... }\n\n// WRONG - Sorting in template every render\n@for (item of items() | sort:'name'; track item.id) { ... }\n```\n\n---\n\n## 7. State Management (MEDIUM)\n\n### Use Selectors to Prevent Re-renders\n\n```typescript\n// CORRECT - Selective subscription\n@Component({\n  template: `<span>{{ userName() }}</span>`,\n})\nclass HeaderComponent {\n  private store = inject(Store);\n  // Only re-renders when userName changes\n  userName = this.store.selectSignal(selectUserName);\n}\n\n// WRONG - Subscribing to entire state\n@Component({\n  template: `<span>{{ state().user.name }}</span>`,\n})\nclass HeaderComponent {\n  private store = inject(Store);\n  // Re-renders on ANY state change\n  state = toSignal(this.store);\n}\n```\n\n### Colocate State with Features\n\n```typescript\n// CORRECT - Feature-scoped store\n@Injectable() // NOT providedIn: 'root'\nexport class ProductStore { ... }\n\n@Component({\n  providers: [ProductStore], // Scoped to component tree\n})\nexport class ProductPageComponent {\n  store = inject(ProductStore);\n}\n\n// WRONG - Everything in global store\n@Injectable({ providedIn: 'root' })\nexport class GlobalStore {\n  // Contains ALL app state - hard to tree-shake\n}\n```\n\n---\n\n## 8. Memory Management (LOW-MEDIUM)\n\n### Use takeUntilDestroyed for Subscriptions\n\n```typescript\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\n\n@Component({...})\nexport class DataComponent {\n  private destroyRef = inject(DestroyRef);\n\n  constructor() {\n    this.data$.pipe(\n      takeUntilDestroyed(this.destroyRef)\n    ).subscribe(data => this.process(data));\n  }\n}\n\n// WRONG - Manual subscription management\nexport class DataComponent implements OnDestroy {\n  private subscription!: Subscription;\n\n  ngOnInit() {\n    this.subscription = this.data$.subscribe(...);\n  }\n\n  ngOnDestroy() {\n    this.subscription.unsubscribe(); // Easy to forget\n  }\n}\n```\n\n### Prefer Signals Over Subscriptions\n\n```typescript\n// CORRECT - No subscription needed\n@Component({\n  template: `<div>{{ data().name }}</div>`,\n})\nexport class Component {\n  data = toSignal(this.service.data$, { initialValue: null });\n}\n\n// WRONG - Manual subscription\n@Component({\n  template: `<div>{{ data?.name }}</div>`,\n})\nexport class Component implements OnInit, OnDestroy {\n  data: Data | null = null;\n  private sub!: Subscription;\n\n  ngOnInit() {\n    this.sub = this.service.data$.subscribe((d) => (this.data = d));\n  }\n\n  ngOnDestroy() {\n    this.sub.unsubscribe();\n  }\n}\n```\n\n---\n\n## Quick Reference Checklist\n\n### New Component\n\n- [ ] `changeDetection: ChangeDetectionStrategy.OnPush`\n- [ ] `standalone: true`\n- [ ] Signals for state (`signal()`, `input()`, `output()`)\n- [ ] `inject()` for dependencies\n- [ ] `@for` with `track` expression\n\n### Performance Review\n\n- [ ] No methods in templates (use pipes or computed)\n- [ ] Large lists virtualized\n- [ ] Heavy components deferred\n- [ ] Routes lazy-loaded\n- [ ] Third-party libs dynamically imported\n\n### SSR Check\n\n- [ ] Hydration configured\n- [ ] Critical content renders first\n- [ ] Non-critical content uses `@defer (hydrate on ...)`\n- [ ] TransferState for server-fetched data\n\n---\n\n## Resources\n\n- [Angular Performance Guide](https://angular.dev/best-practices/performance)\n- [Zoneless Angular](https://angular.dev/guide/experimental/zoneless)\n- [Angular SSR Guide](https://angular.dev/guide/ssr)\n- [Change Detection Deep Dive](https://angular.dev/guide/change-detection)\n\n## When to Use\nThis skill is applicable to execute the workflow or actions described in the overview.",
  "applicable_domains": [
    "frontend"
  ],
  "category": "frontend",
  "invocation": [
    "/angular-best-practices"
  ],
  "authored_by": "claudeskills.in community",
  "source_url": "https://claudeskills.in/skill/angular-best-practices",
  "provenance": {
    "source": "claudeskills.in",
    "source_url": "https://claudeskills.in/skill/angular-best-practices",
    "license": "unknown",
    "imported_at": "2026-09-03",
    "notes": "Aggregated by claudeskills.in from community GitHub lists."
  },
  "tags": [
    "claudeskills",
    "frontend",
    "risk-reviewed"
  ],
  "lifecycle": "draft"
}