angular-best-practices - SKILL
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: selfdate_added: “2026-02-27”Angular最佳实践Angular 应用的综合性能优化指南。包含消除性能瓶颈、优化包和改进渲染的优先规则。何时使用在以下情况下参考这些指南编写新的 Angular 组件或页面实现数据获取模式审查代码的性能问题重构现有的 Angular 代码优化包大小或加载时间配置 SSR/水合按优先级划分的规则类别PriorityCategoryImpactFocus1Change DetectionCRITICALSignals, OnPush, Zoneless2Async WaterfallsCRITICALRxJS patterns, SSR preloading3Bundle OptimizationCRITICALLazy loading, tree shaking4Rendering PerformanceHIGHdefer, trackBy, virtualization5Server-Side RenderingHIGHHydration, prerendering6Template OptimizationMEDIUMControl flow, pipes7State ManagementMEDIUMSignal patterns, selectors8Memory ManagementLOW-MEDIUMCleanup, subscriptions1. 变更检测关键使用 OnPush 变更检测// CORRECT - OnPush with SignalsComponent({changeDetection:ChangeDetectionStrategy.OnPush,template:div{{ count() }}/div,})exportclassCounterComponent{countsignal(0);}// WRONG - Default change detectionComponent({template:div{{ count }}/div,// Checked every cycle})exportclassCounterComponent{count0;}优先使用信号而非可变属性// CORRECT - Signals trigger precise updatesComponent({template:h1{{ title() }}/h1 pCount: {{ count() }}/p,})exportclassDashboardComponent{titlesignal(Dashboard);countsignal(0);}// WRONG - Mutable properties require zone.js checksComponent({template:h1{{ title }}/h1 pCount: {{ count }}/p,})exportclassDashboardComponent{titleDashboard;count0;}为新项目启用无区域模式// 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 {divclasschart-skeleton/div}!-- WRONG - Heavy component in initial bundle --app-analytics-chart[data]data()/避免桶文件的再次导出// WRONG - Imports entire barrel, breaks tree-shakingimport{Button,Modal,Table}fromshared/components;// CORRECT - Direct importsimport{Button}fromshared/components/button/button.component;import{Modal}fromshared/components/modal/modal.component;动态导入第三方库// CORRECT - Load heavy library on demandasyncloadChart(){const{Chart}awaitimport(chart.js);this.chartnewChart(this.canvas,config);}// WRONG - Bundle Chart.js in main chunkimport{Chart}fromchart.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}fromangular/cdk/scrolling;Component({imports:[CdkVirtualScrollViewport,CdkFixedSizeVirtualScroll],template:cdk-virtual-scroll-viewport itemSize50 classviewport div *cdkVirtualForlet item of items classitem {{ item.name }} /div /cdk-virtual-scroll-viewport})优先使用纯管道而非方法// CORRECT - Pure pipe, memoizedPipe({name:filterActive,standalone:true,pure:true})exportclassFilterActivePipeimplementsPipeTransform{transform(items:Item[]):Item[]{returnitems.filter(ii.active);}}// Templatefor(itemofitems()|filterActive;track item.id){...}// WRONG - Method called every change detectionfor(itemofgetActiveItems();track item.id){...}使用 computed() 获取派生数据// CORRECT - Computed, cached until dependencies changeexportclassProductStore{productssignalProduct[]([]);filtersignal();filteredProductscomputed((){constfthis.filter().toLowerCase();returnthis.products().filter(pp.name.toLowerCase().includes(f));});}// WRONG - Recalculates every accessgetfilteredProducts(){returnthis.products.filter(pp.name.toLowerCase().includes(this.filter));}5. 服务器端渲染高配置增量水合// app.config.tsimport{provideClientHydration,withIncrementalHydration,}fromangular/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{privatehttpinject(HttpClient);privatetransferStateinject(TransferState);privateplatformIdinject(PLATFORM_ID);getData(key:string):ObservableData{conststateKeymakeStateKeyData(key);if(isPlatformBrowser(this.platformId)){constcachedthis.transferState.get(stateKey,null);if(cached){this.transferState.remove(stateKey);returnof(cached);}}returnthis.http.getData(/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 {spanGuest/span} for (item of items(); track item.id) {app-item[item]item/} empty {pNo items/p}!-- WRONG - Legacy structural directives --span*ngIfuser; else guest{{ user.name }}/spanng-template#guestspanGuest/span/ng-template避免复杂的模板表达式// CORRECT - Precompute in componentclassComponent{itemssignalItem[]([]);sortedItemscomputed(()[...this.items()].sort((a,b)a.name.localeCompare(b.name)));}// Templatefor(itemofsortedItems();track item.id){...}// WRONG - Sorting in template every renderfor(itemofitems()|sort:name;track item.id){...}7. 状态管理中等使用选择器防止重新渲染// CORRECT - Selective subscriptionComponent({template:span{{ userName() }}/span,})classHeaderComponent{privatestoreinject(Store);// Only re-renders when userName changesuserNamethis.store.selectSignal(selectUserName);}// WRONG - Subscribing to entire stateComponent({template:span{{ state().user.name }}/span,})classHeaderComponent{privatestoreinject(Store);// Re-renders on ANY state changestatetoSignal(this.store);}将状态与特征并置// CORRECT - Feature-scoped storeInjectable()// NOT providedIn: rootexportclassProductStore{...}Component({providers:[ProductStore],// Scoped to component tree})exportclassProductPageComponent{storeinject(ProductStore);}// WRONG - Everything in global storeInjectable({providedIn:root})exportclassGlobalStore{// Contains ALL app state - hard to tree-shake}8. 内存管理低-中对订阅使用 takeUntilDestroyedimport{takeUntilDestroyed}fromangular/core/rxjs-interop;Component({...})exportclassDataComponent{privatedestroyRefinject(DestroyRef);constructor(){this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(datathis.process(data));}}// WRONG - Manual subscription managementexportclassDataComponentimplementsOnDestroy{privatesubscription!:Subscription;ngOnInit(){this.subscriptionthis.data$.subscribe(...);}ngOnDestroy(){this.subscription.unsubscribe();// Easy to forget}}偏好信号而非订阅// CORRECT - No subscription neededComponent({template:div{{ data().name }}/div,})exportclassComponent{datatoSignal(this.service.data$,{initialValue:null});}// WRONG - Manual subscriptionComponent({template:div{{ data?.name }}/div,})exportclassComponentimplementsOnInit,OnDestroy{data:Data|nullnull;privatesub!:Subscription;ngOnInit(){this.subthis.service.data$.subscribe((d)(this.datad));}ngOnDestroy(){this.sub.unsubscribe();}}快速参考清单新组件changeDetection: ChangeDetectionStrategy.OnPush独立: true状态信号signal()、input()、output()用于依赖项的inject()for与track表达式绩效评估模板中没有方法使用管道或计算属性大型列表已虚拟化重型组件推迟路由惰性加载第三方库动态导入SSR 检查已配置水合关键内容先呈现非关键内容使用defer (在...时水合)用于服务器获取数据的 TransferState资源Angular Performance GuideZoneless AngularAngular SSR GuideChange Detection Deep Dive限制仅当任务明确符合上述描述的范围时才使用此技能。不要将输出视为环境特定验证、测试或专家审查的替代品。如果缺少所需的输入、权限、安全界限或成功标准请停下来并要求澄清。