• [方案分享] 如何解决鸿蒙列表滑动加载过程中会出现白块的问题
    一、业务背景在处理大量页面数据的场景下,用户进行快速查询操作时遇到了严重的性能问题:页面加载缓慢,长时间显示白屏,用户交互体验极差。经统计,该问题导致用户使用率降低了 60%,对业务发展产生了显著影响 二、原因分析2.1 数据加载策略失当,内存占用超限原始方案采用ForEach组件一次性加载全量数据(如数百甚至数千条记录),直接将所有数据存入内存并构建组件树:内存压力:大量数据对象(如每条记录包含文本、图片 URL、状态标识等)同时驻留内存,导致 JS 引擎内存占用激增(如 500 条记录可能占用 200MB + 内存),低端设备易触发内存溢出(OOM),引发页面崩溃或强制刷新。加载阻塞:全量数据请求通过单次接口调用完成,若数据量过大(如响应体超过 1MB),网络传输耗时增加(尤其弱网环境下),且接口处理时间延长,超过前端超时阈值(如 5 秒)后,用户看到的仍是 “白屏”。2.1 LazyForEach缓存配置僵化,预加载不足 若cachedCount(预加载缓存项数量)默认值为 1(系统默认),未根据列表项高度和屏幕尺寸动态调整:滑动时白屏:用户快速滑动列表时,视口外的列表项因未被缓存而销毁,滑动至新区域时需重新创建组件并加载资源(如图片),导致短暂白屏(尤其图片占比高的场景)。重复渲染损耗:缓存项不足时,列表项会在 “进入视口→离开视口→再次进入” 的过程中反复销毁与重建,浪费 CPU 资源,加剧滑动卡顿。三、解决思路数据按需精简与结构化传输,在现有懒加载、缓存优化及预加载机制的基础上,从数据源头减少传输与处理成本,进一步缓解前端性能压力,具体思路如下:通过后端接口优化,只传输前端当前场景下必需的数据字段,并采用结构化格式减少冗余信息,从根源上降低网络传输耗时、JS 内存占用及组件渲染压力四、解决方案4.1 列表优化列表页面结构比较复杂,最外层为Refresh组件包裹,第一层子组件为List组件,然后再里面有WaterFlow组件的嵌套,必须保证列表代码结构设计的比较好,不然会容易出现掉帧,卡顿,以及滑动过程中图片内容显示空白等问题。主要是从以下几个方面进行的优化处理:懒加载与缓存策略实现从官方文档可以了解到ForEach是从列表数据源一次性加载全量数据,且一次性并全部挂载在组件树上;LazyForEach是按需加载部分数据,只构建出一棵短小的组件树。针对数据加载和组件树构建这两个显著差异。另外我们还需要知道的是List等组件中cachedCount属性默认为1,所以当使用LazyForEach没有设置缓存项数量的时候,默认的缓存项数量实际为1懒加载代码的实现:import { ObservedArray, Observed } from '@ohos.data.observed';import { IDataSource, DataChangeListener } from '@ohos.app.ability.dataSource';const TAG = '[AdvancedLazyDataSource]';/** * 基础数据源接口实现 * 提供数据变更通知机制 */class BasicDataSource<T> implements IDataSource { private listeners: DataChangeListener[] = []; public totalCount(): number { return 0; } public getData(_index: number): T | undefined { return undefined; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 批量数据变更通知 notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }); } // 单项数据添加通知 notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } // ...}/** * 高级懒加载数据源实现 * 增加了数据分批加载、虚拟滚动支持等功能 */@Observedexport default class AdvancedLazyDataSource<T> extends BasicDataSource<T> { @State dataArray: T[] = []; private pageSize: number = 20; // 每页加载数量 private totalPages: number = 0; // 总页数 private currentPage: number = 1; // 当前页码 private isLoading: boolean = false; // 加载状态 /** * 获取数据总数 */ public totalCount(): number { return this.dataArray.length; } /** * 获取指定位置数据 */ public getData(index: number): T { return this.dataArray[index]; } /** * 添加数据到指定位置 */ public addData(index: number, data: T): void { this.dataArray.splice(index, 0, data); this.notifyDataAdd(index); } /** * 追加数据到末尾 */ public pushData(data: T): void { this.dataArray.push(data); this.notifyDataAdd(this.dataArray.length - 1); } /** * 加载更多数据 * @param page 页码 * @returns 加载结果 */ public async loadMoreData(page: number = this.currentPage + 1): Promise<boolean> { if (this.isLoading || page > this.totalPages) { return false; } this.isLoading = true; try { // 模拟网络请求加载数据 const newData = await this.fetchDataFromServer(page, this.pageSize); // 更新数据源 const startIndex = this.dataArray.length; this.dataArray.push(...newData); // 通知数据变更 for (let i = 0; i < newData.length; i++) { this.notifyDataAdd(startIndex + i); } this.currentPage = page; return true; } catch (error) { console.error(TAG, 'Load more data failed', error); return false; } finally { this.isLoading = false; } } /** * 从服务器获取数据 * @param page 页码 * @param size 每页数量 * @returns 数据数组 */ private async fetchDataFromServer(page: number, size: number): Promise<T[]> { // 实际项目中这里应该是网络请求 // 模拟延迟 await new Promise(resolve => setTimeout(resolve, 300)); // 模拟数据 return Array.from({ length: size }, (_, i) => ({ id: `${page}-${i}`, content: `Item ${page * size + i}` }) as unknown as T); } /** * 重置数据源 */ public reset(): void { this.dataArray = []; this.currentPage = 1; this.isLoading = false; this.notifyDataReload(); }}缓存策略优化LazyForEach 组件的缓存策略对性能影响显著:cachedCount 属性:指定预加载的缓存项数量,默认为 1优化策略:对于一般列表:设置 cachedCount 为 5-10,平衡首屏加载速度和滑动体验对于长列表:根据列表项高度和屏幕高度动态计算 cachedCount对于快速滑动场景:可适当增大 cachedCount,但需测试首屏加载时间4.2 动态预加载(Prefetcher 机制)     HarmonyOS 提供的 Prefetcher 机制具有以下特点动态自适应:根据网络状态智能调整预加载策略按需预取:只预加载视口附近的数据和资源资源缓存:将预加载的资源缓存到本地,减少重复请求请求管理:自动管理预加载请求,避免资源浪费预加载流程:用户滑动列表 → 计算可见区域 → 触发visibleAreaChanged → Prefetcher预取可见区域附近数据 → 缓存资源 → 列表项需要时直接使用缓存资源预加载与懒加载的协同工作:懒加载:解决大数据量下的组件渲染性能问题预加载:解决资源加载延迟导致的白屏问题组合使用:实现 "即见即得" 的流畅体验预加载实现细节创建IDataSourcePrefetching接口的实现类export class CardDataSource implements IDataSourcePrefetching { private dataList: Array<V11BaseCardShowModel> = []; private listeners: DataChangeListener[] = []; private readonly requestsInFlight: HashMap<number, rcp.Request> = new HashMap(); private readonly session: rcp.Session = rcp.createSession(); private readonly cachePath = getContext().getApplicationContext().cacheDir; constructor(showModelList: Array<ShowModel>) { this.dataList = showModelList; } async prefetch(index: number): Promise<void> { const item = this.dataList[index]; if (this.requestsInFlight.hasKey(index)) { return; } if (item.cachedImage != "") { return; } const request = new rcp.Request(item.thumbnail.imageUrl, 'GET'); this.requestsInFlight.set(index, request); try { //提前拉取数据 const response = await this.session.fetch(request); if (response.statusCode !== 200 || !response.body) { Logger.error('CardDataSource response code:' + response.statusCode); return } // 将加载的数据信息存储到缓存文件中 item.cachedImage = await this.cacheImg(item.id, response.body); // 删除指定元素 this.requestsInFlight.remove(index); } catch (err) { if (err.code !== CANCEL_CODE) { //失败就直接使用url链接 item.cachedImage = item.thumbnail.imageUrl; // 移除有异常的网络请求任务 this.requestsInFlight.remove(index); } } }}创建实际使用的IDataSourcePrefetching的实现对象和BasicPrefetcher对象// 创建DataSourcePrefetching对象,具备任务预取、取消能力的数据源@State dataSource: CardDataSource = new CardDataSource(new Array<ShowModel>())private readonly prefetcher = new BasicPrefetcher(this.dataSource);数据刷新时,要立刻刷新BasicPrefetcher中DataSource // LazyForEach的数据源更新 this.dataSource.updateData(showModels) this.prefetcher.setDataSource(this.dataSource); 调用BasicPrefetcher的visibleAreaChanged方法.onScrollIndex((start: number, end: number) => { this.prefetcher.visibleAreaChanged(start, end) })  五、方案总结本方案通过以下核心技术解决性能问题:懒加载技术:使用 LazyForEach 避免一次性加载全量数据缓存策略:优化 cachedCount 参数减少滑动延迟预加载机制:利用 Prefetcher 提前加载资源减少白屏组件优化:简化结构减少渲染开销网络优化:根据网络状态动态调整加载策略