Ionic千级UI组件渲染卡顿优化及加载Spinner实现问询
针对你遇到的渲染1000个自定义组件时的界面卡顿问题,以及需要添加加载状态的需求,我整理了以下针对性的解决方案:
一、缩短渲染耗时的优化方案
1. 使用虚拟滚动(最核心优化)
直接渲染1000个组件会让DOM节点数量暴增,这是卡顿的主要原因。Ionic提供了ion-virtual-scroll组件,它只会渲染当前可视区域内的组件,大幅减少DOM节点数量,从根源解决卡顿问题。
修改你的HTML代码:
<!-- 替换原来的外层*ngFor容器 --> <ion-virtual-scroll [items]="toDisplayArray" itemSize="80"> <!-- itemSize根据你的子组件实际高度设置,比如80px --> <ion-item *virtualItem="let set"> <div [ngSwitch]="set.type"> <ng-container *ngSwitchCase="'range'"> <range [id]="set.id" [title]="set.title"></range> </ng-container> <ng-container *ngSwitchCase="'select'"> <select [id]="set.id" [title]="set.title"></select> </ng-container> <ng-container *ngSwitchCase="'both'"> <range-select [id]="set.id" [title]="set.title"></range-select> </ng-container> </div> </ion-item> </ion-virtual-scroll>
注意:
itemSize必须设置为子组件的固定高度(如果高度不固定,可以用approxItemSize),虚拟滚动才能准确计算可视区域。
2. 优化子组件的变更检测
给你的range、select、range-select子组件设置OnPush变更检测策略,这样只有当输入属性变化时才会触发组件的变更检测,减少不必要的性能消耗:
在每个子组件的.ts文件中:
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'range', templateUrl: './range.component.html', changeDetection: ChangeDetectionStrategy.OnPush // 添加这一行 }) export class RangeComponent { @Input() id: string; @Input() title: string; // ... 其他代码 }
3. 使用trackBy函数提升列表更新性能
在你的页面组件中添加trackBy函数,帮助Angular复用已有的组件实例,而不是每次数据更新都重新创建所有组件:
页面.ts文件:
trackById(index: number, item: any): string { return item.id; // 假设每个item的id是唯一标识 }
然后在模板中(虚拟滚动和普通列表都支持):
<!-- 非虚拟滚动的情况 --> <div *ngFor="let set of toDisplayArray; trackBy: trackById; let i = index"> <!-- ... 内容不变 --> </div> <!-- 虚拟滚动的情况 --> <ion-virtual-scroll [items]="toDisplayArray" itemSize="80" [trackBy]="trackById"> <!-- ... 内容不变 --> </ion-virtual-scroll>
4. 用ngSwitch替代多个*ngIf
原来的模板中用了三个*ngIf判断组件类型,换成ngSwitch可以减少Angular的条件判断次数,提升模板渲染效率:
<div [ngSwitch]="set.type"> <ng-container *ngSwitchCase="'range'"> <range [id]="set.id" [title]="set.title"></range> </ng-container> <ng-container *ngSwitchCase="'select'"> <select [id]="set.id" [title]="set.title"></select> </ng-container> <ng-container *ngSwitchCase="'both'"> <range-select [id]="set.id" [title]="set.title"></range-select> </ng-container> </div>
用ng-container不会额外生成DOM节点,保持DOM结构简洁。
二、添加加载Spinner直至渲染完成
因为数据获取很快,但渲染耗时,我们需要在渲染完成前显示加载状态。可以通过以下方式实现:
1. 基本实现(简单可靠)
在页面中添加加载状态变量,结合setTimeout等待DOM渲染完成:
页面.ts文件:
isLoading = false; onSelect(dbId) { this.isLoading = true; // 开始获取数据时显示加载 this.dbService.getData(dbId).then(data => { this.toDisplayArray = data.set; // 用setTimeout让Angular完成DOM渲染后再隐藏加载 setTimeout(() => { this.isLoading = false; }, 0); }); }
页面HTML文件:
<ion-content> <!-- 显示加载Spinner --> <div *ngIf="isLoading" class="loading-container"> <ion-spinner name="crescent"></ion-spinner> <p>加载中...</p> </div> <!-- 原来的选择器 --> <ion-select interface='popover' (ionChange)='onSelect($event.detail.value)'> <ion-select-option *ngFor="let set of deviceList; let i = index" value="{{set.Id}}"> {{set.name}}</ion-select-option> </ion-select> <!-- 优化后的组件列表(仅在加载完成后显示) --> <ion-virtual-scroll *ngIf="!isLoading" [items]="toDisplayArray" itemSize="80"> <!-- ... 子组件内容 --> </ion-virtual-scroll> </ion-content>
添加CSS样式让Spinner居中:
.loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; }
2. 精准判断渲染完成(进阶)
如果需要更精准地知道渲染完成时间,可以通过ViewChildren监听子组件的数量:
页面.ts文件:
import { ViewChildren, QueryList, AfterViewChecked } from '@angular/core'; // 导入你的子组件类型 import { RangeComponent } from './range.component'; import { SelectComponent } from './select.component'; import { RangeSelectComponent } from './range-select.component'; isLoading = false; @ViewChildren(RangeComponent, SelectComponent, RangeSelectComponent) components: QueryList<any>; ngAfterViewChecked() { if (this.isLoading && this.toDisplayArray && this.components.length === this.toDisplayArray.length) { this.isLoading = false; } } onSelect(dbId) { this.isLoading = true; this.dbService.getData(dbId).then(data => { this.toDisplayArray = data.set; }); }
这种方式会在所有子组件都渲染完成后自动隐藏加载Spinner,比setTimeout更精准。
内容的提问来源于stack exchange,提问作者Amit Gandole

