You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular OnPush策略下仍触发插值调用,大列表性能优化求助

Angular OnPush变更检测下不必要的方法调用问题

我最近在做一个需要创建超1000行表格的项目,结果性能拉胯得不行。为了复现问题,我搭了个极简Angular应用——就用Angular CLI生成的app.component组件,里面放了个可折叠div,还有用*ngFor生成的一堆div。

组件的HTML代码是这样的:

<div style="border:1px solid silver" (click)="toggleDiv()">TEST</div>
<div #divToToggle [ngStyle]="divStyle">
</div>
<div *ngFor="let row of rows">{{getName(row)}}</div>

组件类里我创建了100个带name属性的对象,模板里的getName方法会在控制台打日志,然后返回对象的name属性。另外还有个toggleDiv方法用来控制div的展开和折叠。

现在离谱的地方来了:每次点TEST div触发toggleDiv时,所有行的getName都会被调用一遍!这完全没必要啊,以后加个CSS动画啥的肯定会更卡。而且我明明已经用了ChangeDetectionStrategy.OnPush了,没任何数据变化的情况下,Angular为啥还要跑这些插值操作?

有没有办法能改改这个行为?我想要的是Angular渲染完表格后,除非组件输入参数变了,否则啥都别干。

测试过的Angular版本:4.4.4和5.1.3

组件的TypeScript代码如下:

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
 rows: any[];
 expanded = false;
 divStyle: any;
 expandedStyle = { "height": "200px", "background-color": "orange" }
 collapsedStyle = { "display": "none" }
 constructor() {
 this.divStyle = this.expandedStyle;
 this.expanded = true;
 this.rows = this.createRows(100);
 }
 toggleDiv() {
 this.expanded = !this.expanded;
 if (this.expanded) {
 this.divStyle = this.expandedStyle;
 } else {
 this.divStyle = this.collapsedStyle;
 }
 }
 getName(row:any): string {
 console.log("getName called")
 return row.name;
 }
 private createRows(count:number): any[] {
 let rows: any[] = [];
 for (let i=0; i < count; i++) {
 rows.push({ name: "TEST " + (i+1) })
 }
 return rows;
 }
}

问题出在哪?

你可能以为用了OnPush就万事大吉,但其实OnPush的变更检测还是会在几种情况下触发:组件输入属性变化、组件内部触发了事件(比如你这个click事件)、手动调用ChangeDetectorRef的方法,或者异步操作完成(比如RxJS订阅回调)。

你点击TEST div触发toggleDiv属于组件内部事件,这会直接触发组件的变更检测周期。这时候Angular会遍历模板里的所有绑定——包括插值表达式里的getName方法,所以每次变更检测都会把所有行的getName调用一遍。

解决办法有这几个

1. 别在模板里直接调用方法

模板里的方法每次变更检测都会执行,最好的办法是提前把要显示的值计算好存在数组里。比如修改createRows的时候直接把名称存好,或者在构造函数里预处理:

constructor() {
 this.divStyle = this.expandedStyle;
 this.expanded = true;
 // 直接把需要显示的名称预存在对象里
 this.rows = this.createRows(100).map(row => ({...row, displayName: row.name}));
}

然后模板改成直接读属性:

<div *ngFor="let row of rows">{{row.displayName}}</div>

这样变更检测时只需要读取属性值,不会再执行额外的方法,性能自然就上去了。

2. 手动接管变更检测

如果实在要保留模板里的方法,或者有复杂逻辑,可以用ChangeDetectorRef手动控制变更检测的时机。先注入这个服务,初始化完成后停止自动检测,只有需要更新的时候再手动触发:

import { ChangeDetectorRef, OnInit } from '@angular/core';

export class AppComponent implements OnInit {
 constructor(private cdr: ChangeDetectorRef) {
 // ... 原有代码
 }

 ngOnInit() {
 // 初始化完成后,关掉自动变更检测
 this.cdr.detach();
 }

 toggleDiv() {
 this.expanded = !this.expanded;
 this.divStyle = this.expanded ? this.expandedStyle : this.collapsedStyle;
 // 只在需要更新的时候手动触发一次变更检测
 this.cdr.detectChanges();
 }

 // 以后如果rows数据真的变了,再手动触发检测
 updateRows(newCount: number) {
 this.rows = this.createRows(newCount);
 this.cdr.detectChanges();
 }
}

这样只有你主动调用detectChanges()的时候才会执行变更检测,其他时候不会触发,getName也就不会被无端调用了。

3. 用纯管道替代方法

把getName的逻辑做成纯管道,纯管道只有当输入参数的引用变化时才会重新计算,变更检测时如果输入没变,会直接返回缓存的结果:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'rowName' })
export class RowNamePipe implements PipeTransform {
 transform(row: any): string {
 console.log("Pipe called");
 return row.name;
 }
}

然后模板里改成用管道:

<div *ngFor="let row of rows">{{row | rowName}}</div>

点击toggleDiv时,rows的引用没变化,所以管道不会重新执行,也就不会打印日志了。


内容的提问来源于stack exchange,提问作者Dieter Rehbein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:20:29