HarmonyOS ArkUI 列表与网格长列表的高性能渲染小实践
ArkUI 列表与网格:长列表的高性能渲染
列表是 App 里最常写的界面
资讯流、商品列表、聊天记录、通讯录,几乎每个应用都有列表。ArkUI 里列表用 List + ListItem 构建,网格用 Grid + GridItem,但真正决定性能的是懒加载机制。
基础列表 List
List 是滚动列表容器,ListItem 是每一项。垂直列表是默认行为:
@Entry
@Component
struct ListDemo {
@State items: string[] = ['苹果', '香蕉', '橙子', '葡萄'];
build() {
List() {
ForEach(this.items, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(18)
Blank() // 弹性占位,把右侧内容推到右边
Image($r('app.media.arrow_right'))
.width(20).height(20)
}
.height(60)
.padding({ left: 16, right: 16 })
}
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
}
要点:
ForEach生成列表项,key 用能唯一标识该项的字段ListItem里放每一项的内容Blank()是弹性占位组件,常用于把内容左右分散
懒加载:大数据量的关键
上面的 ForEach 会一次性生成所有 ListItem,数据量小没问题,几千上万条就会卡顿。懒加载 LazyForEach 只渲染可见区域附近的项:
// 数据源实现 IDataSource 接口
class MyDataSource implements IDataSource {
private list: string[] = [];
totalCount(): number {
return this.list.length;
}
getData(index: number): string {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {}
unregisterDataChangeListener(listener: DataChangeListener): void {}
}
@Entry
@Component
struct LazyListDemo {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
// 初始化 10000 条数据
let arr: string[] = [];
for (let i = 0; i < 10000; i++) {
arr.push(`第 ${i} 项`);
}
this.data = new MyDataSource();
(this.data as any).list = arr;
}
build() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
Text(item).height(50)
}
}, (item: string) => item)
}
.cachedCount(3) // 预加载可见区域外的 3 项,减少滚动白屏
}
}
LazyForEach 的核心:
- 数据源要自己实现
IDataSource接口(totalCount+getData) cachedCount设置预加载数量,滚动时提前准备好,避免白屏- 只渲染视口附近的项,10000 条和 100 条性能几乎一样
宫格 Grid
Grid 是网格容器,GridItem 是每个格子。商品列表、相册九宫格都用它:
@Entry
@Component
struct GridDemo {
@State items: string[] = ['1', '2', '3', '4', '5', '6', '7', '8'];
build() {
Grid() {
ForEach(this.items, (item: string) => {
GridItem() {
Column() {
Text(`商品 ${item}`)
.fontSize(16)
}
.width('100%')
.height(140)
.backgroundColor('#F0F0F0')
.borderRadius(8)
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr') // 三列等宽
.columnsGap(12) // 列间距
.rowsGap(12) // 行间距
.padding(12)
}
}
columnsTemplate('1fr 1fr 1fr') 定义三列等宽布局,fr 是弹性单位。改列数就改这段字符串。
列表的交互:点击和分割线
列表项点击和分割线是最常见的交互:
@Entry
@Component
struct ListInteractive {
@State selectedIndex: number = -1;
build() {
List({ space: 8 }) { // space 设置列表项间距
ForEach(['设置', '通知', '关于', '退出'], (item: string, index: number) => {
ListItem() {
Row() {
Text(item).fontSize(16)
}
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(this.selectedIndex === index ? '#FFF0E6' : '#FFFFFF')
}
.onClick(() => {
this.selectedIndex = index; // 点击高亮
})
}, (item: string) => item)
}
.divider({ strokeWidth: 1, color: '#F0F0F0' }) // 分割线
}
}
divider 给列表项之间加分割线,space 是间距,两者二选一按需使用。
轮播 Swiper
Swiper 是轮播容器,banner 图、引导页都用它:
@Entry
@Component
struct SwiperDemo {
private banners: string[] = ['banner1', 'banner2', 'banner3'];
build() {
Swiper() {
ForEach(this.banners, (banner: string) => {
Image($r(`app.media.${banner}`))
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
}, (banner: string) => banner)
}
.autoPlay(true) // 自动播放
.interval(3000) // 间隔 3 秒
.indicator(true) // 显示指示器圆点
.loop(true) // 循环播放
}
}
一个完整案例:商品列表页
把列表、懒加载、点击、图片综合起来:
class GoodsDataSource implements IDataSource {
private list: Goods[] = [];
totalCount(): number { return this.list.length; }
getData(index: number): Goods { return this.list[index]; }
registerDataChangeListener(listener: DataChangeListener): void {}
unregisterDataChangeListener(listener: DataChangeListener): void {}
}
class Goods {
id: number;
name: string;
price: number;
constructor(id: number, name: string, price: number) {
this.id = id;
this.name = name;
this.price = price;
}
}
@Entry
@Component
struct GoodsListPage {
private data: GoodsDataSource = new GoodsDataSource();
aboutToAppear() {
let arr: Goods[] = [];
for (let i = 0; i < 1000; i++) {
arr.push(new Goods(i, `商品${i}`, i * 1.5));
}
(this.data as any).list = arr;
}
build() {
Column() {
// 标题栏
Row() {
Text('商品列表').fontSize(20).fontWeight(FontWeight.Bold)
}
.height(56)
.padding({ left: 16 })
// 商品列表
List() {
LazyForEach(this.data, (goods: Goods) => {
ListItem() {
Row() {
Image($r('app.media.goods_placeholder'))
.width(80).height(80)
.borderRadius(8)
Column() {
Text(goods.name).fontSize(16)
Text(`¥${goods.price}`).fontSize(18).fontColor('#FF6B35')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
Blank()
Button('购买')
.height(32)
.type(ButtonType.Capsule)
}
.height(100)
.padding({ left: 16, right: 16 })
}
.onClick(() => {
console.info(`点击了 ${goods.name}`);
})
}, (goods: Goods) => goods.id.toString())
}
.cachedCount(2)
.divider({ strokeWidth: 1, color: '#F5F5F5' })
}
.width('100%')
.height('100%')
}
}
这个例子涵盖了列表开发的完整套路:数据源实现、懒加载、按需渲染、点击处理、图片展示。
列表性能的几条小经验哦
-
大数据量必须用
LazyForEach。ForEach一次性渲染所有项,几千条就明显卡顿。LazyForEach渲染视口附近,数据量对性能几乎没有影响。 -
key 必须唯一且稳定。
LazyForEach的第三个参数返回唯一 key,框架靠它做增量更新。用id字段,别用数组下标(删除项时下标会变,导致更新错乱)。 -
cachedCount别设太大。预加载项数设 1-3 即可,设太大会一次性渲染太多项,反而增加内存和首屏延迟。 -
列表项里避免重组件。单个
ListItem里别塞太复杂的布局和动效,列表滚动时每项都要布局,项越重滚动越卡。 -
Blank是布局利器。左右分散内容时用Blank(),比手写 margin 更灵活,适配不同屏宽。
更多推荐

所有评论(0)