HarmonyOS 鸿蒙Next数学类上架项目解析32-列表渲染性能与内存优化
HarmonyOS 鸿蒙Next数学类上架项目解析32-列表渲染性能与内存优化 在开发鸿蒙数学计算应用时,遇到列表性能问题:
- 长列表滚动卡顿
- 大量数据渲染导致内存占用过高
- 列表项更新时整个列表重新渲染
- 快速滚动时出现白屏或闪烁
如何优化ArkTS应用中的列表渲染性能和内存占用?
目前有明确的文档介绍。列表高性能实现方式一般有以下方式:
- 使用@Reusable + LazyForEach,或者@Reusable + Repeat。
- 使用@ReusableV2 + LazyForEach,或者@ReusableV2 + Repeat,这套组合也是推荐的。
上面两种是列表处理方式。不过列表卡顿,还得考虑item的组件渲染性能,item里面是否考虑到组件复用,组件层级是否可降低,组件是否可进行最小化渲染等。
文档参考:长列表加载丢帧优化-界面渲染性能优化-性能场景优化案例-性能 - 华为HarmonyOS开发者 (huawei.com)
文档参考:Repeat-状态管理与渲染控制-ArkTS组件-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者 (huawei.com)
更多关于HarmonyOS 鸿蒙Next数学类上架项目解析32-列表渲染性能与内存优化的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
列表性能问题的核心是减少不必要的渲染和内存占用。需要使用懒加载、虚拟列表、正确的key策略等优化手段。
1. 常见性能问题分析
// 问题1:没有使用唯一key,导致整个列表重新渲染
@Component
struct BadList {
@State items: string[] = []
build() {
List() {
// 错误:使用index作为key,删除/插入时会导致大量重渲染
ForEach(this.items, (item: string, index: number) => {
ListItem() {
Text(item)
}
}, (item: string, index: number) => index.toString()) // 错误的key
}
}
}
// 问题2:列表项组件过于复杂
@Component
struct HeavyListItem {
@Prop data: ComplexData
build() {
Column() {
// 大量嵌套和计算,每次渲染都很耗时
ForEach(this.data.subItems, (sub: SubItem) => {
Row() {
Image(sub.icon).width(50).height(50)
Column() {
Text(sub.title)
Text(this.formatDate(sub.date)) // 每次渲染都计算
Text(this.calculatePrice(sub)) // 每次渲染都计算
}
}
})
}
}
}
// 问题3:一次性加载所有数据
@Component
struct LoadAllAtOnce {
@State items: DataItem[] = []
aboutToAppear(): void {
// 一次性加载10000条数据,内存爆炸
this.items = this.loadAllData()
}
}
2. 使用LazyForEach实现懒加载
// 数据源接口实现
class ListDataSource implements IDataSource {
private data: DataItem[] = []
private listeners: DataChangeListener[] = []
constructor(data: DataItem[]) {
this.data = data
}
totalCount(): number {
return this.data.length
}
getData(index: number): DataItem {
return this.data[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
}
// 通知数据变化
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
// 数据操作方法
addData(item: DataItem): void {
this.data.push(item)
this.notifyDataAdd(this.data.length - 1)
}
deleteData(index: number): void {
this.data.splice(index, 1)
this.notifyDataDelete(index)
}
updateData(index: number, item: DataItem): void {
this.data[index] = item
this.notifyDataChange(index)
}
reloadData(newData: DataItem[]): void {
this.data = newData
this.notifyDataReload()
}
}
interface DataItem {
id: string
title: string
content: string
}
// 使用LazyForEach
@Entry
@Component
struct OptimizedList {
private dataSource: ListDataSource = new ListDataSource([])
aboutToAppear(): void {
// 初始化数据
const initialData: DataItem[] = []
for (let i = 0; i < 10000; i++) {
initialData.push({
id: `item_${i}`,
title: `标题 ${i}`,
content: `内容 ${i}`
})
}
this.dataSource = new ListDataSource(initialData)
}
build() {
List() {
// LazyForEach只渲染可见区域的项
LazyForEach(this.dataSource, (item: DataItem) => {
ListItem() {
OptimizedListItem({ item: item })
}
}, (item: DataItem) => item.id) // 使用唯一ID作为key
}
.width('100%')
.height('100%')
.cachedCount(5) // 缓存前后5个项
}
}
3. 列表项组件优化
//优化的列表项组件
@Component
struct OptimizedListItem {
@Prop item: DataItem
// 缓存计算结果
@State private formattedDate: string = ''
aboutToAppear(): void {
// 只在组件创建时计算一次
this.formattedDate = this.formatDate(this.item.timestamp)
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp)
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}
build() {
Row() {
// 使用固定尺寸,避免布局计算
Column() {
Text(this.item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.item.content)
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 })
Text(this.formattedDate)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.height(80) // 固定高度,提升滚动性能
.padding(12)
}
}
interface DataItem {
id: string
title: string
content: string
timestamp: number
}
4. 分页加载
//分页加载管理器
class PaginationManager {
private pageSize: number = 20
private currentPage: number = 0
private hasMore: boolean = true
private isLoading: boolean = false
constructor(pageSize: number = 20) {
this.pageSize = pageSize
}
// 加载下一页
async loadNextPage(
loadFn: (page: number, size: number) => Promise<DataItem[]>
): Promise<DataItem[]> {
if (this.isLoading || !this.hasMore) {
return []
}
this.isLoading = true
try {
const data = await loadFn(this.currentPage, this.pageSize)
if (data.length < this.pageSize) {
this.hasMore = false
}
this.currentPage++
return data
} finally {
this.isLoading = false
}
}
// 重置分页
reset(): void {
this.currentPage = 0
this.hasMore = true
this.isLoading = false
}
// 是否还有更多数据
canLoadMore(): boolean {
return this.hasMore && !this.isLoading
}
}
@Entry
@Component
struct PaginatedList {
@State items: DataItem[] = []
@State isLoading: boolean = false
@State hasMore: boolean = true
private pagination: PaginationManager = new PaginationManager(20)
aboutToAppear(): void {
this.loadMore()
}
async loadMore(): Promise<void> {
if (!this.pagination.canLoadMore()) return
this.isLoading = true
const newItems = await this.pagination.loadNextPage(
async (page, size) => {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 500))
const items: DataItem[] = []
const start = page size
for (let i = 0; i < size; i++) {
items.push({
id: `item_${start + i}`,
title: `标题 ${start + i}`,
content: `内容 ${start + i}`,
timestamp: Date.now()
})
}
return items
}
)
this.items = [...this.items, ...newItems]
this.hasMore = this.pagination.canLoadMore()
this.isLoading = false
}
build() {
List() {
ForEach(this.items, (item: DataItem) => {
ListItem() {
OptimizedListItem({ item: item })
}
}, (item: DataItem) => item.id)
// 加载更多指示器
if (this.hasMore) {
ListItem() {
Row() {
if (this.isLoading) {
LoadingProgress().width(24).height(24)
Text('加载中...').margin({ left: 8 })
} else {
Text('上拉加载更多')
}
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
}
}
.width('100%')
.height('100%')
.onReachEnd(() => {
this.loadMore()
})
}
}
5. 列表项复用与缓存
//图片缓存管理
class ImageCache {
private static cache: Map<string, string> = new Map()
private static maxSize: number = 100
static get(url: string): string | undefined {
return this.cache.get(url)
}
static set(url: string, data: string): void {
// LRU策略:超出限制时删除最早的
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(url, data)
}
static clear(): void {
this.cache.clear()
}
}
//带缓存的列表项
@Component
struct CachedListItem {
@Prop item: ItemWithImage
@State imageLoaded: boolean = false
aboutToAppear(): void {
// 检查缓存
const cached = ImageCache.get(this.item.imageUrl)
if (cached) {
this.imageLoaded = true
}
}
build() {
Row() {
// 图片占位符
Stack() {
if (this.imageLoaded) {
Image(this.item.imageUrl)
.width(60)
.height(60)
.borderRadius(8)
} else {
Column()
.width(60)
.height(60)
.backgroundColor('#f0f0f0')
.borderRadius(8)
}
}
Column() {
Text(this.item.title)
.fontSize(16)
Text(this.item.subtitle)
.fontSize(14)
.fontColor('#666666')
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(12)
}
}
interface ItemWithImage {
id: string
title: string
subtitle: string
imageUrl: string
}
6. 虚拟滚动优化
/ 虚拟滚动列表 只渲染可见区域的项,大幅减少DOM节点 /
@Entry
@Component
struct VirtualScrollList {
@State visibleItems: DataItem[] = []
@State scrollOffset: number = 0
private allItems: DataItem[] = []
private itemHeight: number = 80
private containerHeight: number = 600
private bufferCount: number = 5
aboutToAppear(): void {
// 初始化大量数据
for (let i = 0; i < 10000; i++) {
this.allItems.push({
id: `item_${i}`,
title: `标题 ${i}`,
content: `内容 ${i}`,
timestamp: Date.now()
})
}
this.updateVisibleItems()
}
// 计算可见项
updateVisibleItems(): void {
const startIndex = Math.max(0,
Math.floor(this.scrollOffset / this.itemHeight) - this.bufferCount
)
const endIndex = Math.min(
this.allItems.length,
Math.ceil((this.scrollOffset + this.containerHeight) / this.itemHeight) + this.bufferCount
)
this.visibleItems = this.allItems.slice(startIndex, endIndex).map((item, index) => ({
...item,
_virtualIndex: startIndex + index
}))
}
build() {
Scroll() {
Column() {
// 顶部占位
Column()
.height(this.getTopPadding())
// 只渲染可见项
ForEach(this.visibleItems, (item: DataItem & { _virtualIndex: number }) => {
Row() {
Text(item.title)
.fontSize(16)
}
.width('100%')
.height(this.itemHeight)
.padding(12)
.backgroundColor(item._virtualIndex % 2 === 0 ? '#ffffff' : '#f9f9f9')
}, (item: DataItem) => item.id)
// 底部占位
Column()
.height(this.getBottomPadding())
}
}
.width('100%')
.height(this.containerHeight)
.onScroll((xOffset: number, yOffset: number) => {
this.scrollOffset += yOffset
this.updateVisibleItems()
})
}
getTopPadding(): number {
const startIndex = Math.max(0,
Math.floor(this.scrollOffset / this.itemHeight) - this.bufferCount
)
return startIndex this.itemHeight
}
getBottomPadding(): number {
const endIndex = Math.min(
this.allItems.length,
Math.ceil((this.scrollOffset + this.containerHeight) / this.itemHeight) + this.bufferCount
)
return (this.allItems.length - endIndex) this.itemHeight
}
}
7. 性能监控
//列表性能监控
class ListPerformanceMonitor {
private renderTimes: number[] = []
private scrollFPS: number[] = []
private lastFrameTime: number = 0
//记录渲染时间
recordRenderTime(time: number): void {
this.renderTimes.push(time)
if (this.renderTimes.length > 100) {
this.renderTimes.shift()
}
}
//记录滚动帧率
recordScrollFrame(): void {
const now = Date.now()
if (this.lastFrameTime > 0) {
const fps = 1000 / (now - this.lastFrameTime)
this.scrollFPS.push(fps)
if (this.scrollFPS.length > 60) {
this.scrollFPS.shift()
}
}
this.lastFrameTime = now
}
//获取平均渲染时间
getAverageRenderTime(): number {
if (this.renderTimes.length === 0) return 0
return this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length
}
// 获取平均帧率
getAverageFPS(): number {
if (this.scrollFPS.length === 0) return 0
return this.scrollFPS.reduce((a, b) => a + b, 0) / this.scrollFPS.length
}
// 输出性能报告
report(): void {
console.info(`[性能报告] 平均渲染时间: ${this.getAverageRenderTime().toFixed(2)}ms`)
console.info(`[性能报告] 平均帧率: ${this.getAverageFPS().toFixed(1)} FPS`)
}
}
总结
列表渲染性能优化的关键点:
- 使用LazyForEach实现懒加载,只渲染可见项
- 使用唯一且稳定的key,避免不必要的重渲染
- 列表项使用固定高度,提升滚动性能
- 缓存计算结果,避免重复计算
- 实现分页加载,避免一次性加载大量数据
- 使用图片缓存,减少重复加载
- 设置合适的cachedCount缓存数量
- 监控性能指标,持续优化
- 项目链接:https://gitee.com/solgull/math-fbox
针对HarmonyOS Next中ArkTS列表渲染的性能与内存优化,核心在于利用框架提供的懒加载、复用机制和精细化更新能力。以下是具体优化方案:
1. 长列表卡顿优化
- 使用
LazyForEach替代ForEach进行数据源绑定,仅渲染可视区域内的列表项,大幅减少首次渲染和滚动时的节点数量。 - 确保
LazyForEach与ListItem或ListItemGroup配合使用,并正确实现onAppear/onDisappear生命周期回调,及时释放不可见项的资源。
2. 内存占用过高处理
- 通过
cachedCount属性(如List组件)设置合理的预加载项数,平衡滚动流畅性与内存开销,避免缓存过多不可见项。 - 对于复杂列表项,使用
@Reusable装饰器标记可复用的自定义组件,配合框架的组件复用机制减少内存重复分配。 - 在
aboutToReuse和aboutToRecycle回调中手动管理重用时的高开销资源(如图片内存释放)。
3. 避免全列表重新渲染
- 为列表项组件添加
key参数,确保数据更新时框架能精准定位差异项,仅更新必要节点。 - 使用状态管理(如
@State、@Link)的局部更新特性,避免顶层数据变更触发整个列表刷新。 - 对于动态列表,采用
List组件的onScrollIndex事件监听可视区域变化,实现数据分片加载与更新。
4. 滚动白屏/闪烁解决
- 启用
listDirection与layoutWeight优化布局计算,减少滚动时的布局重排。 - 为图片资源设置合适尺寸或使用
Image的interpolation控制加载质量,避免滚动时解码阻塞。 - 通过
blankCount属性(瀑布流等场景)预留滚动占位空间,配合LazyForEach保持滚动连续性。
补充建议
- 复杂列表项应拆分为独立组件,结合
@Component与@Builder按需构建内容。 - 使用
performance模块监控滚动帧率与内存峰值,针对性调整缓存策略。 - 对于超长列表,考虑数据分页加载或虚拟滚动方案,进一步降低渲染压力。
以上方法直接基于ArkUI框架能力,可系统性提升列表性能与内存效率。


