一、为什么Angular应用会变慢?

想象你正在用Angular开发一个电商网站,当用户滚动商品列表时,页面开始卡顿。这种情况很可能是变更检测策略出了问题。Angular默认会在任何可能影响视图的操作后检查整个组件树,就像有个保安在每个房间门口不断检查:"这里变了吗?那里变了吗?"

举个例子,假设我们有个实时显示股票价格的组件:

// 技术栈:Angular 16
@Component({
  selector: 'app-stock-ticker',
  template: `
    <div *ngFor="let stock of stocks">
      {{stock.name}}: {{stock.price | currency}}
    </div>
  `,
  // 这里使用了默认的变更检测策略
  changeDetection: ChangeDetectionStrategy.Default 
})
export class StockTickerComponent {
  stocks = [
    {name: 'AAPL', price: 185.15},
    {name: 'MSFT', price: 328.39}
  ];

  constructor() {
    // 每秒钟更新股票价格
    setInterval(() => {
      this.stocks.forEach(stock => {
        stock.price *= (1 + (Math.random() - 0.5) * 0.02);
      });
    }, 1000);
  }
}

这个组件的问题在于:虽然我们只修改了股票价格,但Angular会检查整个组件树。如果页面上还有其他几十个组件,它们也会被不必要地检查。

二、认识两种检测策略

Angular提供了两种主要的变更检测策略:

  1. Default策略:默认模式,任何异步事件(点击、定时器、HTTP请求等)都会触发整个组件树的检查
  2. OnPush策略:只有当输入属性变化或组件触发事件时才会检查

让我们改造上面的股票组件:

@Component({
  selector: 'app-stock-ticker',
  template: `...`, // 模板同上
  // 关键修改:使用OnPush策略
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StockTickerComponent {
  // 必须使用不可变数据更新方式
  updatePrices() {
    this.stocks = this.stocks.map(stock => ({
      ...stock,
      price: stock.price * (1 + (Math.random() - 0.5) * 0.02)
    }));
  }
}

这里的关键区别是:

  • 我们不再直接修改数组元素,而是创建新数组
  • 只有stocks引用变化时才会触发变更检测
  • 性能提升可能达到50%以上

三、OnPush策略的实战技巧

3.1 处理对象和数组

使用OnPush时,必须注意数据更新的方式。错误的做法:

// ❌ 错误:直接修改数组元素不会触发变更
this.stocks[0].price = newPrice; 

// ✅ 正确:创建新引用
this.stocks = [...this.stocks];
this.stocks[0] = {...this.stocks[0], price: newPrice};

3.2 配合Async管道

Async管道是OnPush策略的最佳搭档:

@Component({
  selector: 'app-news',
  template: `
    <div *ngFor="let item of news$ | async">
      {{item.title}}
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class NewsComponent {
  // 使用Observable配合async管道
  news$ = this.http.get('/api/news');

  constructor(private http: HttpClient) {}
}

这种方式自动处理订阅和取消订阅,且只在有新数据时更新视图。

3.3 手动触发变更检测

有时我们需要手动通知Angular:

constructor(private cdr: ChangeDetectorRef) {}

updateData() {
  this.dataService.getData().subscribe(data => {
    this.data = data;
    // 手动标记需要检查
    this.cdr.markForCheck(); 
  });
}

四、性能优化实战案例

让我们看一个完整的大型列表优化案例:

// 技术栈:Angular 16
@Component({
  selector: 'app-product-list',
  template: `
    <app-product-item 
      *ngFor="let product of products; trackBy: trackById"
      [product]="product"
      (addToCart)="onAddToCart($event)">
    </app-product-item>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
  products: Product[] = [];

  // 关键:使用trackBy优化ngFor
  trackById(index: number, item: Product) {
    return item.id; 
  }

  onAddToCart(productId: string) {
    // 处理购物车逻辑...
  }
}

// 子组件
@Component({
  selector: 'app-product-item',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductItemComponent {
  @Input() product!: Product;
  @Output() addToCart = new EventEmitter<string>();
}

这个例子结合了:

  1. OnPush策略减少检测次数
  2. trackBy优化列表渲染
  3. 不可变数据流
  4. 事件驱动变更

五、什么时候该用哪种策略?

适用Default策略的场景:

  • 小型应用或简单组件
  • 需要频繁更新的实时数据展示
  • 组件树层级很浅的情况

适用OnPush策略的场景:

  • 大型应用性能敏感区域
  • 展示型组件(纯UI组件)
  • 数据流清晰可控的组件
  • 使用Redux/NgRx等状态管理的应用

六、常见陷阱与解决方案

陷阱1:忘记使用不可变更新

// ❌ 错误
this.user.profile.avatar = newUrl; 

// ✅ 正确
this.user = {
  ...this.user,
  profile: {
    ...this.user.profile,
    avatar: newUrl
  }
};

陷阱2:在异步回调中忘记标记变更

setTimeout(() => {
  this.counter++;
  // 需要手动触发
  this.cdr.markForCheck(); 
}, 1000);

陷阱3:过度使用markForCheck

// ❌ 滥用标记
data$.subscribe(data => {
  this.data = data;
  this.cdr.markForCheck(); // 其实async管道更好
});

// ✅ 更好的方式
<ng-container *ngIf="data$ | async as data">
  <!-- 使用data -->
</ng-container>

七、进阶优化技巧

  1. Zone.js优化:通过NgZone.runOutsideAngular执行不涉及UI更新的代码
constructor(private ngZone: NgZone) {}

startAnalytics() {
  this.ngZone.runOutsideAngular(() => {
    // 这里面的代码不会触发变更检测
    setInterval(() => this.collectStats(), 1000);
  });
}
  1. 组件设计原则

    • 将智能组件(带逻辑)和展示组件(纯UI)分离
    • 展示组件全部使用OnPush策略
    • 通过服务共享状态而非层层传递@Input
  2. 性能监测工具

    • 使用Angular DevTools的Profiler选项卡
    • 在Chrome性能面板记录交互过程
    • 检测变更检测周期耗时

八、总结与最佳实践

经过上面的分析,我们可以得出以下性能优化路线图:

  1. 评估现状:先用DevTools找出性能瓶颈
  2. 策略选择:对性能敏感组件采用OnPush
  3. 数据流改造:使用不可变数据和纯函数
  4. 列表优化:添加trackBy函数
  5. 异步处理:优先使用async管道
  6. 手动控制:必要时使用ChangeDetectorRef
  7. 持续监测:每次改动后验证性能提升

记住,变更检测策略不是非此即彼的选择。在一个大型应用中,你可以:

  • 对80%的展示型组件使用OnPush
  • 保留Default策略给确实需要频繁更新的组件
  • 通过良好的组件设计隔离变更范围

最后要强调的是:不要为了优化而优化。只有当确实出现性能问题时,才需要考虑这些策略。过早优化可能会增加不必要的复杂度。