如何优化Angular渲染性能?嵌套ngFor渲染大数据集卡顿问题
你遇到的这个问题我太熟悉了——400+条数据带嵌套循环,Angular的模板渲染确实会直接卡成PPT。嵌套的*ngFor肯定是拖慢性能的重要原因,但单条400+的列表本身也会触发大量的变更检测和DOM操作,咱们一步步拆解优化:
1. 用虚拟滚动砍掉90%的DOM节点(最核心优化)
Angular CDK提供的虚拟滚动组件CdkVirtualScrollViewport可以只渲染当前可视区域内的列表项,不管你有400条还是4000条,页面上永远只存在十几二十个DOM节点,性能提升立竿见影。
不过原生<table>和虚拟滚动的适配需要调整结构,把<tbody>替换成虚拟滚动容器:
首先在模块里导入ScrollingModule:
import { ScrollingModule } from '@angular/cdk/scrolling'; @NgModule({ imports: [ // ...其他导入 ScrollingModule ] }) export class YourModule { }
然后修改模板结构:
<table class="tableDefault"> <thead> <!-- 表头部分保持不变 --> </thead> <tbody cdkVirtualScrollViewport itemSize="60"> <!-- itemSize设为每行的实际高度,根据你的样式调整 --> <tr *cdkVirtualFor="let p of rows; let i = index; trackBy: trackRowById"> <td class="tableDefault__td tableDefault__center td__number"> {{ i + 1 }} </td> <td class="tableDefault__td tableDefault__center td__date"> <app-history-item *ngFor="let h of p.history; trackBy: trackHistoryById" [historyItem]="h" (click)="goto(h)"></app-history-item> </td> <td class="tableDefault__td tableDefault__center td__theme"> <input [disabled]="!p.isOwner" type="text" name="subject" placeholder="{{ 'create_plan_e_name' | translate }}" [(ngModel)]="p.topic" /> </td> </tr> </tbody> </table>
在组件类里添加trackBy函数,帮助Angular识别哪些项需要更新:
trackRowById(index: number, row: any): number { return row.id; // 用行数据的唯一ID,没有的话用index也可以 } trackHistoryById(index: number, history: any): number { return history.id; // 用历史项的唯一ID }
2. 把嵌套循环的内容抽成独立组件,开启OnPush变更检测
嵌套的*ngFor会让每个历史项都成为当前组件变更检测的一部分,抽成独立组件后,我们可以给它设置OnPush变更检测策略,只有当输入的historyItem变化时才会重新渲染,大大减少不必要的检测。
创建HistoryItemComponent:
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-history-item', template: ` <div class="history_item"> <span class="span_link" (click)="onClick()">{{ historyItem.classNumber }}{{ historyItem.classSuffix }}, {{ historyItem.formattedWhenDate }}</span> </div> `, changeDetection: ChangeDetectionStrategy.OnPush // 关键:开启OnPush,减少变更检测频率 }) export class HistoryItemComponent { @Input() historyItem: any; @Output() click = new EventEmitter(); onClick(): void { this.click.emit(); } }
3. 预处理数据,避免模板里的函数调用
模板里的formatDate(h.whenDate)和isOwner(teacherId)会在每次变更检测时都执行一遍,400条数据加嵌套的历史项,这会产生大量重复计算。我们提前在组件里预处理这些数据:
// 假设你在获取rows数据后执行这段代码 this.rows.forEach(row => { // 预处理格式化后的日期,避免模板重复调用formatDate row.history.forEach(history => { history.formattedWhenDate = this.formatDate(history.whenDate); }); // 预处理是否为所有者的状态,避免模板重复调用isOwner row.isOwner = this.isOwner(this.teacherId); });
然后模板里直接用预处理好的属性,不用再调用函数。
4. 给父组件也开启OnPush变更检测
如果整个列表组件也设置OnPush策略,那么只有当rows的引用发生变化(比如重新赋值)或者组件的@Input属性变化时,才会触发组件的变更检测,进一步减少性能消耗:
@Component({ // ...其他配置(selector、template等) changeDetection: ChangeDetectionStrategy.OnPush }) export class YourListComponent { // ...组件逻辑 }
总结
这些优化组合起来,虚拟滚动解决了大量DOM节点的问题,组件拆分+OnPush减少了变更检测的范围,trackBy避免了不必要的重渲染,预处理数据消除了模板里的重复计算。400+数据的渲染速度应该会有质的提升。
内容的提问来源于stack exchange,提问作者POV

