☰
angular-best-practices - SKILL
2026/10/26 1:42:10 网站建设 项目流程

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最佳实践

Angular 应用的综合性能优化指南。包含消除性能瓶颈、优化包和改进渲染的优先规则。

何时使用

在以下情况下参考这些指南:

  • 编写新的 Angular 组件或页面
  • 实现数据获取模式
  • 审查代码的性能问题
  • 重构现有的 Angular 代码
  • 优化包大小或加载时间
  • 配置 SSR/水合

按优先级划分的规则类别

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. 变更检测(关键)

使用 OnPush 变更检测

// CORRECT - OnPush with Signals@Component({changeDetection:ChangeDetectionStrategy.OnPush,template:`<div>{{ count() }}</div>`,})exportclassCounterComponent{count=signal(0);}// WRONG - Default change detection@Component({template:`<div>{{ count }}</div>`,// Checked every cycle})exportclassCounterComponent{count=0;}

优先使用信号而非可变属性

// CORRECT - Signals trigger precise updates@Component({template:`<h1>{{ title() }}</h1> <p>Count: {{ count() }}</p>`,})exportclassDashboardComponent{title=signal("Dashboard");count=signal(0);}// WRONG - Mutable properties require zone.js checks@Component({template:`<h1>{{ title }}</h1> <p>Count: {{ count }}</p>`,})exportclassDashboardComponent{title="Dashboard";count=0;}

为新项目启用无区域模式

// main.ts - Zoneless Angular (v20+)bootstrapApplication(AppComponent,{providers:[provideZonelessChangeDetection()],});

好处:

  • 在异步 API 上不使用 zone.js 补丁
  • 较小的捆绑包(约节省15KB)
  • 清理调试用的堆栈跟踪
  • 更好的微前端兼容性

2. 异步操作与瀑布流(关键)

消除顺序数据获取

// WRONG - Nested subscriptions create waterfallsthis.route.params.subscribe((params)=>{// 1. Wait for paramsthis.userService.getUser(params.id).subscribe((user)=>{// 2. Wait for userthis.postsService.getPosts(user.id).subscribe((posts)=>{// 3. Wait for posts});});});// CORRECT - Parallel execution with forkJoinforkJoin({user:this.userService.getUser(id),posts:this.postsService.getPosts(id),}).subscribe((data)=>{// Fetched in parallel});// CORRECT - Flatten dependent calls with switchMapthis.route.params.pipe(map((p)=>p.id),switchMap((id)=>this.userService.getUser(id)),).subscribe();

在 SSR 中避免客户端瀑布流

// CORRECT - Use resolvers or blocking hydration for critical dataexportconstroute:Route={path:"profile/:id",resolve:{data:profileResolver},// Fetched on server before navigationcomponent:ProfileComponent,};// WRONG - Component fetches data on initclassProfileComponentimplementsOnInit{ngOnInit(){// Starts ONLY after JS loads and component rendersthis.http.get("/api/profile").subscribe();}}

3. 捆绑优化(关键)

懒加载路由

// CORRECT - Lazy load feature routesexportconstroutes: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 everythingimport{AdminModule}from"./admin/admin.module";exportconstroutes:Routes=[{path:"admin",component:AdminComponent},// In main bundle];

对重组件使用 @defer

<!-- CORRECT - Heavy component loads on demand -->@defer (on viewport) {<app-analytics-chart[data]="data()"/>} @placeholder {<divclass="chart-skeleton"></div>}<!-- WRONG - Heavy component in initial bundle --><app-analytics-chart[data]="data()"/>

避免桶文件的再次导出

// WRONG - Imports entire barrel, breaks tree-shakingimport{Button,Modal,Table}from"@shared/components";// CORRECT - Direct importsimport{Button}from"@shared/components/button/button.component";import{Modal}from"@shared/components/modal/modal.component";

动态导入第三方库

// CORRECT - Load heavy library on demandasyncloadChart(){const{Chart}=awaitimport('chart.js');this.chart=newChart(this.canvas,config);}// WRONG - Bundle Chart.js in main chunkimport{Chart}from'chart.js';

4. 渲染性能(高)

在 @for 中始终使用 trackBy

<!-- 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"/>}

对大列表使用虚拟滚动

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>`})

优先使用纯管道而非方法

// CORRECT - Pure pipe, memoized@Pipe({name:'filterActive',standalone:true,pure:true})exportclassFilterActivePipeimplementsPipeTransform{transform(items:Item[]):Item[]{returnitems.filter(i=>i.active);}}// Template@for(itemofitems()|filterActive;track item.id){...}// WRONG - Method called every change detection@for(itemofgetActiveItems();track item.id){...}

使用 computed() 获取派生数据

// CORRECT - Computed, cached until dependencies changeexportclassProductStore{products=signal<Product[]>([]);filter=signal('');filteredProducts=computed(()=>{constf=this.filter().toLowerCase();returnthis.products().filter(p=>p.name.toLowerCase().includes(f));});}// WRONG - Recalculates every accessgetfilteredProducts(){returnthis.products.filter(p=>p.name.toLowerCase().includes(this.filter));}

5. 服务器端渲染(高)

配置增量水合

// app.config.tsimport{provideClientHydration,withIncrementalHydration,}from"@angular/platform-browser";exportconstappConfig:ApplicationConfig={providers:[provideClientHydration(withIncrementalHydration(),withEventReplay()),],};

延迟非关键内容

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

使用 TransferState 进行 SSR 数据

@Injectable({providedIn:"root"})exportclassDataService{privatehttp=inject(HttpClient);privatetransferState=inject(TransferState);privateplatformId=inject(PLATFORM_ID);getData(key:string):Observable<Data>{conststateKey=makeStateKey<Data>(key);if(isPlatformBrowser(this.platformId)){constcached=this.transferState.get(stateKey,null);if(cached){this.transferState.remove(stateKey);returnof(cached);}}returnthis.http.get<Data>(`/api/${key}`).pipe(tap((data)=>{if(isPlatformServer(this.platformId)){this.transferState.set(stateKey,data);}}),);}}

6. 模板优化(中等)

使用新的控制流语法

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

避免复杂的模板表达式

// CORRECT - Precompute in componentclassComponent{items=signal<Item[]>([]);sortedItems=computed(()=>[...this.items()].sort((a,b)=>a.name.localeCompare(b.name)));}// Template@for(itemofsortedItems();track item.id){...}// WRONG - Sorting in template every render@for(itemofitems()|sort:'name';track item.id){...}

7. 状态管理(中等)

使用选择器防止重新渲染

// CORRECT - Selective subscription@Component({template:`<span>{{ userName() }}</span>`,})classHeaderComponent{privatestore=inject(Store);// Only re-renders when userName changesuserName=this.store.selectSignal(selectUserName);}// WRONG - Subscribing to entire state@Component({template:`<span>{{ state().user.name }}</span>`,})classHeaderComponent{privatestore=inject(Store);// Re-renders on ANY state changestate=toSignal(this.store);}

将状态与特征并置

// CORRECT - Feature-scoped store@Injectable()// NOT providedIn: 'root'exportclassProductStore{...}@Component({providers:[ProductStore],// Scoped to component tree})exportclassProductPageComponent{store=inject(ProductStore);}// WRONG - Everything in global store@Injectable({providedIn:'root'})exportclassGlobalStore{// Contains ALL app state - hard to tree-shake}

8. 内存管理(低-中)

对订阅使用 takeUntilDestroyed

import{takeUntilDestroyed}from'@angular/core/rxjs-interop';@Component({...})exportclassDataComponent{privatedestroyRef=inject(DestroyRef);constructor(){this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data=>this.process(data));}}// WRONG - Manual subscription managementexportclassDataComponentimplementsOnDestroy{privatesubscription!:Subscription;ngOnInit(){this.subscription=this.data$.subscribe(...);}ngOnDestroy(){this.subscription.unsubscribe();// Easy to forget}}

偏好信号而非订阅

// CORRECT - No subscription needed@Component({template:`<div>{{ data().name }}</div>`,})exportclassComponent{data=toSignal(this.service.data$,{initialValue:null});}// WRONG - Manual subscription@Component({template:`<div>{{ data?.name }}</div>`,})exportclassComponentimplementsOnInit,OnDestroy{data:Data|null=null;privatesub!:Subscription;ngOnInit(){this.sub=this.service.data$.subscribe((d)=>(this.data=d));}ngOnDestroy(){this.sub.unsubscribe();}}

快速参考清单

新组件

  • changeDetection: ChangeDetectionStrategy.OnPush
  • 独立: true
  • 状态信号(signal()、input()、output())
  • 用于依赖项的inject()
  • @for与track表达式

绩效评估

  • 模板中没有方法(使用管道或计算属性)
  • 大型列表已虚拟化
  • 重型组件推迟
  • 路由惰性加载
  • 第三方库动态导入

SSR 检查

  • 已配置水合
  • 关键内容先呈现
  • 非关键内容使用@defer (在...时水合)
  • 用于服务器获取数据的 TransferState

资源

  • Angular Performance Guide
  • Zoneless Angular
  • Angular SSR Guide
  • Change Detection Deep Dive

限制

  • 仅当任务明确符合上述描述的范围时,才使用此技能。
  • 不要将输出视为环境特定验证、测试或专家审查的替代品。
  • 如果缺少所需的输入、权限、安全界限或成功标准,请停下来并要求澄清。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询