一、痛点场景:全量更新阻塞主线程
当接口返回不可拆分的大数据集(如几千条数据,每条数据数十个属性)时,使用Repeat/LazyForEach渲染会遇到:
- 必须在主线程操作:渲染依赖在主线程创建@Observed可观察对象
- 双重性能消耗:O(n²)复杂度(千级卡片×多属性更新)
- 线程切换失效:跨线程通信的序列化开销抵消优化收益
整体流程:
此时想优化渲染卡顿,第一时间可能想到的是使用taskPool或worker,通过并发处理数据,避免主线程渲染卡顿,然而在此场景下,由于线程之间无共享内存,线程之间数据同步还涉及序列化和反序列化操作,且线程间通信对象不支持@State装饰器、@Prop装饰器、@Link装饰器等装饰器修饰的复杂类型,数据在其他线程处理完之后,还得回到主线程创建可观察的数据,切换线程只会带来额外的序列化反序列化开销,对整体性能并无优化效果。
问题核心矛盾
无法避免主线程操作:渲染必须使用主线程创建的可观察对象
遍历+赋值双重耗时:O(n²)时间复杂度(1000卡片×数十属性)
数据传递开销:跨线程通信成本可能抵消优化收益
二、优化思路
-
分离数据计算与渲染 后台线程处理数据计算,差异化部分单独通知到主线程更新(避免在主线程合并全量数据)
-
减少主线程单次工作量 合并更新批次 + 分帧渲染
方案 | 效果 | 成本 |
---|---|---|
跨线程数据计算 | 理论最优 | 需重构数据流架构 |
分帧批量更新 | 显著改善卡顿 | 改动量最小 |
跨线程数据计算虽然理论优化效果最好,但由于需要对代码架构做较大的重构,且实际嵌套的数据结构可能处理起来也会遇到各种坑,因此不在本次尝试范围,分帧、批量更新数据,将大量数据列表的foreach操作,从一整块耗时操作拆分到每一帧之间处理,对整体代码架构的冲击较小,优化成本最低。
2.1 先来看下已有代码结构和实际运行时的trace:
- CardListComponent
import { CardItemComponent } from "./CardItemComponent";
import { CardModel } from "./CardModel";
import { CardViewModel } from "./CardViewModel"
import { displaySync } from "@kit.ArkGraphics2D";
import { ToastUtil } from "@pura/harmony-utils";
const TAG: string = 'CardListComponent'
@Entry
@ComponentV2
export struct CardListComponent {
private timerId: number = -1;
private readonly FRAME_60: number = 60;
private readonly FRAME_120: number = 120;
private startTime: number = 0;
@Local viewModel: CardViewModel = CardViewModel.getInstance();
aboutToAppear(): void {
console.log(TAG, `aboutToAppear, listLenght: ${this.viewModel.cardModelList.length}`);
this.timerId = setTimeout(() => {
this.viewModel.updateListData()
}, 3000)
}
aboutToDisappear(): void {
console.log(TAG, `onHidden`)
}
build() {
Column() {
Grid() {
Repeat<CardModel>(this.viewModel.cardModelList).each((repeatItem) => {
GridItem() {
CardItemComponent({repeatCardObj: repeatItem})
}
.width('48%')
.height('128vp')
})
.key((item) => JSON.stringify(item))
.virtualScroll({})
}
.padding({
left: '8vp',
right: '8vp'
})
.columnsGap('8vp')
.rowsGap('8vp')
.columnsTemplate('1fr 1fr')
.layoutDirection(GridDirection.Column)
.width('100%')
.height('100%')
}
.backgroundColor($r('sys.color.comp_background_gray'))
.width('100%')
.height('100%')
}
}
-
CardItemComponent
import { CardModel } from "./CardModel" import { util } from "@kit.ArkTS"; const TAG: string = 'CardItemComponent' @ComponentV2 export struct CardItemComponent { @Param @Require repeatCardObj: RepeatItem<CardModel>; aboutToAppear(): void { console.log(TAG, `aboutToAppear: ${this.repeatCardObj.item.name}`) } build() { Column({space: '8vp'}) { Row() { Image(this.repeatCardObj.item.icon) .width('50vp') .height('50vp') .interpolation(ImageInterpolation.None) .onClick((event) => { console.log(TAG, `onClick: itemId: ${this.repeatCardObj.item.id}, itemName: ${this.repeatCardObj.item.name}, itemHash: ${util.getHash(this.repeatCardObj.item)}`) }) } .margin('8vp') .width('100%') .height('auto') .justifyContent(FlexAlign.Start) Text(this.repeatCardObj.item.name) .fontColor($r('sys.color.font_primary')) .fontSize('16vp') .margin({left: '8vp'}) Text(this.repeatCardObj.item.desc) .fontColor($r('sys.color.font_secondary')) .fontSize('12vp') .margin({left: '8vp'}) } .alignItems(HorizontalAlign.Start) .backgroundColor($r('sys.color.comp_background_list_card')) .borderRadius('16vp') .width('100%') .height('100%') } }
-
CardModel
@ObservedV2 export class CardModel { @Trace id: number = 0; @Trace icon: string | Resource = ''; @Trace name: string = ''; @Trace desc: string = ''; }
-
CardViewModel
import { ToastUtil } from "@pura/harmony-utils"; import { CardModel } from "./CardModel"; const ICON_LIST: Array<Resource> = []; // 自己本地初始化一个Resource列表 const MAX_LIST_LENGHT: number = 150000; const BATCH_SIZE: number = MAX_LIST_LENGHT/1000; @ObservedV2 export class CardViewModel { private static instance: CardViewModel; @Trace cardModelList: Array<CardModel> = new Array<CardModel>(); private timerId: number = 0; private updateCount: number = 0; public static getInstance(): CardViewModel { if (!CardViewModel.instance) { CardViewModel.instance = new CardViewModel(); } return CardViewModel.instance; } constructor() { this.initCardModelList(); } // 同步更新所有 public updateListData() { let start: number = Date.now(); // 模拟更新已有数据 this.cardModelList.forEach((cardModel) => { let tempModel = new CardModel(); tempModel.icon = ICON_LIST[CardViewModel.getRandomInt(0, ICON_LIST.length - 1)]; cardModel.icon = tempModel.icon; }); ToastUtil.showToast(`列表(${this.cardModelList.length})更新成功, 耗时 ${Date.now() - start} ms`); } initCardModelList() { for (let i = 0; i < MAX_LIST_LENGHT; i++) { let cardModel: CardModel = new CardModel(); cardModel.id = i; cardModel.icon = ICON_LIST[CardViewModel.getRandomInt(0, ICON_LIST.length -1)] cardModel.name = `NameOfCard_${i}` cardModel.desc = `DescOfCard_${i}` this.cardModelList.push(cardModel) } ToastUtil.showToast(`列表(${this.cardModelList.length})初始化成功.`) } public static getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } }
问题代码:
// CardViewModel.ts
public updateListData() {
this.cardModelList.forEach(cardModel => { // 全量遍历
cardModel.icon = getRandomIcon(); // 同步更新所有对象属性
});
}
从Profiler抓到的trace来看,foreach操作占据了大约900ms,在这期间,存在大量的丢帧情况,页面滑动直接无响应。当前模拟的场景150000条数据,每条数据只刷新一个属性,如果实际场景中对象结构复杂,需要刷新的属性多,可能耗时会更长。
接下来的优化思路就很简单了,把这个大量数据的foreach操作拆分开,平均分摊到每一帧刷新之间,避免集中在某一时刻阻塞主线程渲染。
2.2 优化后的代码和trace:
- CardListComponent:
import { CardItemComponent } from "./CardItemComponent";
import { CardModel } from "./CardModel";
import { CardViewModel } from "./CardViewModel"
import { displaySync } from "@kit.ArkGraphics2D";
import { ToastUtil } from "@pura/harmony-utils";
const TAG: string = 'CardListComponent'
@Entry
@ComponentV2
export struct CardListComponent {
private timerId: number = -1;
private readonly FRAME_60: number = 60;
private readonly FRAME_120: number = 120;
private displaySync: displaySync.DisplaySync | undefined = undefined;
private refreshIndex: number = 0; // 当前处理到的索引
private isRefreshing: boolean = false; // 刷新状态标志
private readonly batchSize: number = 100; // 每帧处理的卡片数量
private startTime: number = 0;
@Local viewModel: CardViewModel = CardViewModel.getInstance();
aboutToAppear(): void {
console.log(TAG, `aboutToAppear, listLenght: ${this.viewModel.cardModelList.length}`);
// Creating a DisplaySync Object
this.displaySync = displaySync.create();
// Set the expected frame rate
let range: ExpectedFrameRateRange = {
expected: this.FRAME_120,
min: this.FRAME_60,
max: this.FRAME_120
};
this.displaySync.setExpectedFrameRateRange(range);
ToastUtil.showToast(`3s后开始刷新数据, 启用分帧刷新: ${CardViewModel.getInstance().enableFrameUpdate}`)
this.timerId = setTimeout(() => {
console.log(TAG, `frameUpdate start`);
this.startTime = Date.now();
this.isRefreshing = true;
// Enable frame callback listening
this.displaySync?.on('frame', this.processBatch)
this.displaySync?.start();
}, 3000)
}
aboutToDisappear(): void {
console.log(TAG, `onHidden`)
this.stopBatchRefresh();
}
// 每帧处理一批数据
private processBatch = () => {
if (!this.isRefreshing) return;
const startIdx = this.refreshIndex;
const endIdx = Math.min(startIdx + this.batchSize, this.viewModel.cardModelList.length);
this.viewModel.updateOnFrame(startIdx, endIdx);
this.refreshIndex = endIdx;
// 检查是否完成
if (this.refreshIndex >= this.viewModel.cardModelList.length) {
ToastUtil.showToast(`刷新结束, index: ${this.refreshIndex}, 耗时: ${Date.now() - this.startTime} ms`)
this.stopBatchRefresh();
}
}
// 停止刷新并清理
stopBatchRefresh() {
this.isRefreshing = false;
this.displaySync?.off('frame', this.processBatch);
}
...其他代码省略
}
CardViewModel:
... 其他代码省略
// 分帧刷新
updateOnFrame(startIdx: number, endIdx: number) {
if (startIdx > this.cardModelList.length || endIdx > this.cardModelList.length) {
console.log("CardViewModel", `enter return, startIdx: ${startIdx}, endIndex: ${endIdx}, length: ${this.cardModelList.length}`);
return;
}
// 处理当前批次
for (let i = startIdx; i < endIdx; i++) {
const cardModel = this.cardModelList[i];
const newIcon = ICON_LIST[CardViewModel.getRandomInt(0, ICON_LIST.length - 1)];
cardModel.icon = newIcon;
}
}
...
可以看到,分帧刷新后,数据刷新从集中的900~1000ms均匀分摊到12s内,掉帧情况大大改善,滑动页面也感受不到卡顿的情况。
2.3 实践建议
-
动态批处理:根据displaySync返回的帧间隔动态调整BATCH_SIZE
-
内存管理:及时清理已完成批次的对象引用
-
中断处理:页面隐藏时停止刷新任务
-
性能平衡点:单帧处理量建议50-200条(需实测)
▶️ 优化本质:将同步阻塞操作 → 异步分帧执行,用时间换流畅度
有其他更优解决方案欢迎指导交流