FORMA

运行时优化

变更检测 策略正确的前提下,减少模板计算与 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 与虚拟滚动

ts
@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 处理高频事件

ts
@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延迟加载区块

Series

performance

2 / 2

变更检测