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

如何优化Angular渲染性能?嵌套ngFor渲染大数据集卡顿问题

Angular大型列表渲染优化:解决400+数据的慢渲染问题

你遇到的这个问题我太熟悉了——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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:21:48