angular-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular Best Practices
Angular最佳实践
Comprehensive performance optimization guide for Angular applications. Contains prioritized rules for eliminating performance bottlenecks, optimizing bundles, and improving rendering.
Angular应用性能优化综合指南,包含消除性能瓶颈、优化包体积及提升渲染效率的优先级规则。
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
在以下场景中参考本指南:
- 编写新的Angular组件或页面
- 实现数据获取模式
- 评审代码以排查性能问题
- 重构现有Angular代码
- 优化包体积或加载时间
- 配置SSR/水合机制
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 | 变更检测 | 关键 | Signals、OnPush、无Zone.js模式 |
| 2 | 异步请求瀑布流 | 关键 | RxJS模式、SSR预加载 |
| 3 | 包体积优化 | 关键 | 懒加载、摇树优化 |
| 4 | 渲染性能优化 | 高 | @defer、trackBy、虚拟滚动 |
| 5 | 服务端渲染(SSR) | 高 | 水合机制、预渲染 |
| 6 | 模板优化 | 中 | 控制流、管道 |
| 7 | 状态管理 | 中 | Signals模式、选择器 |
| 8 | 内存管理 | 中低 | 资源清理、订阅管理 |
1. Change Detection (CRITICAL)
1. 变更检测(关键)
Use OnPush Change Detection
使用OnPush变更检测策略
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;
}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
优先使用Signals而非可变属性
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;
}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
新项目启用无Zone.js模式
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
typescript
// main.ts - Zoneless Angular (v20+)
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});优势:
- 无需对异步API进行zone.js补丁
- 包体积减小约15KB
- 调试时栈追踪更清晰
- 微前端兼容性更好
2. Async Operations & Waterfalls (CRITICAL)
2. 异步操作与请求瀑布流(关键)
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();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
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();
}
}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)
3. 包体积优化(关键)
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
];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
对重型组件使用@defer
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()" />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";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';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)
4. 渲染性能优化(高)
Always Use trackBy with @for
@for循环始终使用trackBy
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" />
}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>
`
})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) { ... }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
使用computed()处理派生数据
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)
);
}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)
5. 服务端渲染(SSR)优化(高)
Configure Incremental Hydration
配置增量水合
typescript
// app.config.ts
import {
provideClientHydration,
withIncrementalHydration,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration(), withEventReplay()),
],
};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 />
}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
使用TransferState共享SSR数据
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);
}
}),
);
}
}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)
6. 模板优化(中)
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>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) { ... }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)
7. 状态管理(中)
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);
}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
}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)
8. 内存管理(中低)
Use takeUntilDestroyed for Subscriptions
使用takeUntilDestroyed管理订阅
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
}
}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
优先使用Signals而非手动订阅
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();
}
}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() - for dependencies
inject() - with
@forexpressiontrack
-
changeDetection: ChangeDetectionStrategy.OnPush -
standalone: true - 使用Signals管理状态(、
signal()、input())output() - 使用注入依赖
inject() - 循环添加
@for表达式track
Performance Review
性能评审
- No methods in templates (use pipes or computed)
- Large lists virtualized
- Heavy components deferred
- Routes lazy-loaded
- Third-party libs dynamically imported
- 模板中不使用方法(改用管道或computed)
- 大型列表启用虚拟滚动
- 重型组件使用延迟加载
- 路由启用懒加载
- 第三方库动态导入
SSR Check
SSR检查
- Hydration configured
- Critical content renders first
- Non-critical content uses
@defer (hydrate on ...) - TransferState for server-fetched data
- 已配置水合机制
- 关键内容优先渲染
- 非关键内容使用
@defer (hydrate on ...) - 服务端获取的数据使用TransferState共享