angular-best-practices

Angular性能优化与最佳实践指南。适用于编写、审查或重构Angular代码时,以实现最佳性能、最小化打包体积和提升渲染效率。

查看详情
name:angular-best-practicesdescription:Angular performance optimization and best practices guide. Use when writing, reviewing, or refactoring Angular code for optimal performance, bundle size, and rendering efficiency.risk:safesource:self

Angular Best Practices

Comprehensive performance optimization guide for Angular applications. Contains prioritized rules for eliminating performance bottlenecks, optimizing bundles, and improving rendering.

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

  • Rule Categories by Priority

    PriorityCategoryImpactFocus
    1Change DetectionCRITICALSignals, OnPush, Zoneless
    2Async WaterfallsCRITICALRxJS patterns, SSR preloading
    3Bundle OptimizationCRITICALLazy loading, tree shaking
    4Rendering PerformanceHIGH@defer, trackBy, virtualization
    5Server-Side RenderingHIGHHydration, prerendering
    6Template OptimizationMEDIUMControl flow, pipes
    7State ManagementMEDIUMSignal patterns, selectors
    8Memory ManagementLOW-MEDIUMCleanup, subscriptions


    1. Change Detection (CRITICAL)

    Use OnPush Change Detection

    // 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

    // 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

    // 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

    // 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

    // 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

    // 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

    <!-- 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

    // 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

    // 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

    <!-- 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

    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

    // 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

    // 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

    // app.config.ts
    import {
    provideClientHydration,
    withIncrementalHydration,
    } from "@angular/platform-browser";

    export const appConfig: ApplicationConfig = {
    providers: [
    provideClientHydration(withIncrementalHydration(), withEventReplay()),
    ],
    };

    Defer Non-Critical Content

    <!-- 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

    @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

    <!-- 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

    // 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

    // 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

    // 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

    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

    // 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

  • Zoneless Angular

  • Angular SSR Guide

  • Change Detection Deep Dive

    1. angular-best-practices - Agent Skills