运行时优化
在 变更检测 策略正确的前提下,减少模板计算与 DOM 规模。运行时性能优化大体分三类:减少变更检测触发次数、减少每次检测的工作量、减少 DOM 节点与重排重绘。本文按这三条主线展开,并给出可直接落地的清单。
核心概念
Angular 的渲染流水线大致是:事件/异步回调触发 → 变更检测遍历组件树 → diff 绑定表达式 → 更新 DOM。性能问题通常出现在:
- 检测触发过于频繁(如高频
mousemove、WebSocket 推送); - 单次检测工作量过大(模板里做了昂贵计算、大列表全量 diff);
- DOM 规模过大或频繁重建(未复用节点、
*ngFor缺少稳定 key)。
实践清单
| 手段 | 说明 |
|---|---|
OnPush | 展示型、列表项组件 |
track in @for | 稳定唯一标识,避免 DOM 复用错误 |
| 纯管道 | 无状态管道结果可被缓存 |
| 懒加载路由 | loadComponent / loadChildren |
| 避免模板中重计算 | 用 computed 或组件 getter 缓存 |
| 虚拟滚动 | 长列表使用 CDK Virtual Scroll |
NgZone.runOutsideAngular | 高频、与 UI 无关的回调跳出 Zone 检测 |
trackByFn / track 表达式 | 列表重排时避免整体重渲染 |
示例:@for 的 track 与虚拟滚动
@Component({
standalone: true,
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="48" class="viewport">
<div *cdkVirtualFor="let item of items; trackBy: trackById" class="row">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
@for (item of items; track item.id) {
<app-item-card [item]="item" />
}
`,
})
export class ListComponent {
items = signal<Item[]>([]);
trackById = (_: number, item: Item) => item.id;
}
track item.id(新语法@for)与 CDK 的trackBy作用相同:告诉 Angular 「哪个数据对应哪个 DOM 节点」,重排/增删时只移动/新建必要的节点。- 缺省或使用索引作为 track,在排序、过滤后会导致节点被错误复用,出现动画错乱、输入框内容串位等问题。
示例:跳出 Zone 处理高频事件
@Component({ standalone: true, template: `<div #canvas></div>` })
export class ChartComponent {
private zone = inject(NgZone);
ngAfterViewInit() {
this.zone.runOutsideAngular(() => {
window.addEventListener("mousemove", this.onMove);
});
}
private onMove = (e: MouseEvent) => {
// 纯 Canvas 绘制,不触碰 Angular 绑定的数据,无需回到 Zone
this.draw(e.clientX, e.clientY);
};
}
若回调中需要更新绑定到模板的数据,再显式 this.zone.run(() => this.value.set(x)),避免整棵树被反复检测。
变更检测分析
开发模式可使用 Angular DevTools(浏览器扩展)查看组件树与检测周期,定位「谁触发了这次检测」「哪些组件被检查了但没有变化」。若某组件频繁出现在检测列表却从不更新 DOM,通常说明可以改为 OnPush 或拆分成更细的子组件。
构建体积
- 生产构建默认开启优化与 tree-shaking,务必使用
ng build --configuration production。 - 仅导入使用的
@angular/material子模块(MatButtonModule而非整包)。 - 分析 bundle:官方 构建分析 或
source-map-explorer等工具,重点关注第三方库是否被完整打入。 - 对不常访问的路由使用
loadComponent/loadChildren做路由级代码分割,减少首屏 JS。
最佳实践
- 默认给列表项、展示型组件加
OnPush,容器组件保持Default也可接受,逐步收紧。 - 模板中避免调用返回新对象/新数组的方法(如每次渲染都
.filter()),改用computed或组件内缓存的 getter。 - 大表格、聊天记录、日志流等长列表必须虚拟滚动,否则 DOM 节点数会随数据线性增长。
- 图片、地图等重资源组件配合
@defer(Angular 17+)延迟到可视区域再渲染。 - 定时器、订阅在组件销毁时清理(
takeUntilDestroyed),避免检测无意义地被触发。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 列表滚动卡顿 | 未虚拟滚动、每项组件未 OnPush | 引入 CDK Virtual Scroll + OnPush |
| 输入框内容跳到别的行 | @for/*ngFor 缺少稳定 track/trackBy | 用业务唯一 id 作为 track |
| 页面「卡一下」在拖拽/滚动时 | 高频事件回调在 Zone 内触发大量检测 | runOutsideAngular + 必要时手动 run |
| 生产包体积异常大 | 全量导入 UI 库、未按需 import | 检查 bundle 分析报告,按需引入 |
| DevTools 里同一组件反复被检查却无变化 | 组件仍是 Default 策略 | 改为 OnPush 并确保输入不可变 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| 运行时性能 | 官方 |
| 懒加载 | 路由 |
| Angular DevTools | 调试 |
| CDK Virtual Scroll | 虚拟滚动 |
| @defer | 延迟加载区块 |
相关文章
变更检测
Angular 在数据变化后更新 DOM 的过程称为 变更检测(Change Detection)。理解默认策略、Zone.js 机制、OnPush 与 Signal 协同,有助于性能调优。运行时技巧见 runtime。
运行时优化
减少不必要的渲染与响应式开销。构建侧见 build。
运行时优化
减少不必要的组件重渲染与昂贵计算。涵盖重渲染成因、memo/useMemo/useCallback、虚拟滚动、Web Worker、Profiler 分析、最佳实践与常见坑。构建侧见 build。
生态集成
Angular 官方维护路由、HTTP、表单、动画等;UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方/半官方库、常用第三方库、与 Vue/React 生态对照、选型建议与常见坑。
Signal 基础
Signals 是 Angular 16 引入的响应式原语:可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新,涵盖 API、input signal、与变更检测的关系、最佳实践与常见坑。
RxJS 互操作
Angular 内置 RxJS 处理异步流(HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等)。Signal 适合同步/短链路状态;长生命周期流仍常用 Observable。涵盖 async 管道、toSignal/toObservable、常用操作符、最佳实践与常见坑。
Series
performance
2 / 2