Angular 性能优化:变更检测策略
Angular 应用在运行时会频繁检查数据变化,以更新视图。这个过程叫“变更检测”。默认情况下,Angular 对每个组件都执行完整的变更检测,即使数据没变也会重复检查,这可能导致性能问题,尤其在大型应用中。通过调整变更检测策略,可以显著减少不必要的检查,提升响应速度。
理解默认的变更检测行为
Angular 默认使用 Default 策略。每当事件(如点击、HTTP 响应、定时器)触发时,框架会从根组件开始,递归检查所有组件的绑定值是否发生变化。
查看当前策略:在组件类中打印 ChangeDetectorRef 的 isDefaultStrategy() 方法返回值:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-example',
template: `<p>示例组件</p>`
})
export class ExampleComponent {
constructor(private cdRef: ChangeDetectorRef) {
console.log(this.cdRef.constructor.name); // 通常是 ViewRef_
// 注意:isDefaultStrategy 不是公开 API,仅用于理解
}
}
虽然不能直接调用 isDefaultStrategy(它是内部方法),但你可以通过行为观察:只要父组件触发变更检测,子组件无论是否需要都会被检查。
切换到 OnPush 策略
OnPush 是一种更高效的变更检测策略。启用后,组件只在以下三种情况触发变更检测:
- 输入属性(
@Input())引用发生变化(不是值变化,而是对象/数组的引用变了)。 - 组件内部触发了事件(如
(click))。 - 使用了
async管道订阅的 Observable 或 Promise 发出新值。
启用 OnPush:
-
导入
ChangeDetectionStrategy:import { ChangeDetectionStrategy, Component } from '@angular/core'; -
在组件装饰器中设置
changeDetection:@Component({ selector: 'app-user-card', template: ` <div> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class UserCardComponent { @Input() user!: { name: string; email: string }; }
现在,只有当父组件传入一个全新的 user 对象(而非修改原对象属性)时,UserCardComponent 才会重新检查。
正确使用 OnPush 的关键实践
避免可变数据操作
如果父组件这样更新用户:
// ❌ 错误:修改原对象属性,引用未变
this.user.name = 'New Name';
子组件不会更新,因为 user 对象的引用没变。
改为创建新对象:
// ✅ 正确:创建新引用
this.user = { ...this.user, name: 'New Name' };
或使用不可变数据结构库(如 Immer、Immutable.js)。
主动触发变更检测(必要时)
某些场景下,组件状态变化不来自输入或事件(例如 WebSocket 消息、第三方库回调)。此时需手动通知 Angular。
-
注入
ChangeDetectorRef:constructor(private cdRef: ChangeDetectorRef) {} -
在数据更新后调用
markForCheck():onWebSocketMessage(data: any) { this.localData = data; this.cdRef.markForCheck(); // 标记该组件及其祖先需要检查 }
注意:不要用 detectChanges(),它会立即触发局部检测,可能破坏 OnPush 的优化逻辑;markForCheck() 更安全,它将组件加入下次全局检测队列。
结合 async 管道自动优化
async 管道天然兼容 OnPush。它会自动订阅 Observable,并在新值到达时标记组件为 dirty(需检查)。
在模板中使用 async 管道:
<app-user-card [user]="user$ | async"></app-user-card>
```
其中 `user$` 是一个 `Observable<{ name: string; email: string }>`。
这样,每当 `user$` 推送新值,Angular 会:
- 创建新引用(Observable 值本身是新对象)
- 自动触发 `UserCardComponent` 的变更检测
无需手动调用 `markForCheck()`。
---
## 验证优化效果
使用 Angular DevTools 浏览器插件(Chrome 扩展)检查变更检测频率:
1. **打开 DevTools**,切换到 **Angular** 面板。
2. **启用** “Highlight updates when components render”。
3. **操作应用**,观察哪些组件高亮。
- 启用 OnPush 后,无关组件不应高亮。
4. **检查** “Profiler” 选项卡,记录变更检测周期耗时。
若发现 OnPush 组件仍频繁高亮,检查:
- 是否意外修改了输入属性的内部状态(应始终替换整个对象)。
- 是否在非事件上下文中更新了组件状态但未调用 `markForCheck()`。
---
## 常见误区与解决方案
| 问题现象 | 原因 | 解决方案 |
| :--- | :---: | :--- |
| OnPush 组件不更新 | 修改了输入对象的属性,但引用未变 | 使用展开运算符 `{...obj}` 或 `Object.assign` 创建新对象 |
| 手动调用 `detectChanges()` 后仍无效 | 在错误时机调用(如构造函数中) | 在 `ngAfterViewInit` 或异步回调中调用,并优先用 `markForCheck()` |
| 子组件未随父组件输入更新 | 父组件未传递新引用 | 确保父组件每次更新都生成新对象 |
---
## 何时不应使用 OnPush
并非所有组件都适合 OnPush。避免用于以下情况:
- 组件依赖全局服务状态(且服务未通过 Observable 暴露)。
- 组件包含复杂表单,需实时响应用户输入(但可通过 `async` + 表单控件解决)。
- 组件大量使用 `ViewChild` 或直接操作 DOM,状态难以追踪。
对这类组件,保持默认策略更稳妥。性能优化应聚焦于高频渲染或深层嵌套的组件(如列表项、卡片)。
---
**设置** `changeDetection: ChangeDetectionStrategy.OnPush` **并确保输入数据不可变**。
**在非标准事件中更新状态时,调用** `markForCheck()`。
**优先使用** `async` **管道处理异步数据流**。
暂无评论,快来抢沙发吧!