ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React Native FlatList 在 OpenHarmony 上的虚拟化性能优化实践

React Native FlatList 在 OpenHarmony 上的虚拟化性能优化实践 1. 先聊聊这个优化任务的背景React Native 在 OpenHarmony 上跑起来第一个要面对的问题就是列表。我试过直接把一套电商首页的 FlatList 代码从安卓端搬过来数据量大概几百条带图片的卡片结果在 OpenHarmony 真机上滑动直接掉帧快速滚到底部的时候新行出现有明显的白块和跳动。不是网络问题也不是图片加载问题纯粹是列表渲染扛不住。这个项目的核心就是解决这个场景React Native 的 FlatList 在 OpenHarmony 上怎么把虚拟化真正落地。RN 的 FlatList 本身就是基于虚拟化思路设计的但你在安卓上能流畅跑不代表在 OpenHarmony 上也能流畅跑因为底层宿主环境不一样桥接机制、UI 刷新方式、内存管理都存在差异。如果你正准备把 RN 应用移植到 OpenHarmony或者已经在做但列表性能不达标这篇文章提到的思路和排查方向应该能帮你省很多弯路。我会从两个层面来讲一是 FlatList 虚拟化的底层逻辑为什么它需要虚拟化、虚拟化到底优化了什么二是这份优化实践里真正有参考价值的改动点包括 render 函数治理、组件层级瘦身、图片加载时机、OpenHarmony 特有问题适配等。最后会附上我踩过的几个坑和排查思路。2. 虚拟化列表的核心思路拆解2.1 FlatList 的懒渲染机制到底做了什么FlatList 的虚拟化不是只渲染屏幕内的行这么简单。它的核心逻辑是把一维数据列表映射成有限个行的渲染任务屏幕可视区加上一定的缓冲区默认是屏幕高度的若干倍之外的行直接不创建原生视图。这里有个关键点FlatList 复用的是 ScrollView 的滚动容器但它通过 VirtualizedList 这个基础组件维护了一个状态机。当滚动发生时它计算当前偏移量结合 onLayout 反馈的容器尺寸算出哪些行在可视范围内然后调度渲染、卸载或者回收。这种机制保证的是哪怕你有十万条数据真正在原生层存在的视图节点可能只有三四十个。不过在 OpenHarmony 上这个视图节点数量可控的假设需要重新验证。我打点看过同一个列表在安卓端原生视图数量稳定在 25 个左右在 OpenHarmony 端有时候会冲到 60 个以上。原因后面会细说但如果你只在 JS 层看逻辑永远发现不了这个问题。2.2 props 变化和渲染函数的稳定性FlatList 优化时最容易忽略的是 renderItem 函数本身的状态。我在项目里做了一次改动把 renderItem 从内联箭头函数改成 useCallback 包裹的稳定函数同时把列表项组件用 memo 缓存。听起来是 React 优化的老套路但在这个场景下效果非常明显。原因是 FlatList 在滚动过程中频繁调用 renderItem如果这个函数每次都是新引用列表项组件即使 props 没变化也会重新渲染。JS 层的重新渲染会通过 bridge 同步到原生层在 OpenHarmony 上这一来一回的开销比安卓更大。改完以后我对比了 JS 线程的耗时滚动过程中renderItem 的调用频率没有变但单个 item 的 render 耗时从平均 18ms 降到了 6ms 左右。这还是在没有做任何原生层改动的前提下纯 JS 层面的优化就能把渲染耗时降到三分之一。2.3 为什么虚拟化必须在窗口上做文章FlatList 有个参数叫 windowSize默认值是 21单位是可视区的倍数。也就是说默认大约会保留可视区上方 10 个屏幕高度、下方 10 个屏幕高度的行。在快速滚动的时候这个缓冲窗口是必要的不然用户会看到白屏。但窗口越大同时存在的行越多内存和渲染压力都更大。在 OpenHarmony 上我把 windowSize 从 21 调到了 10 甚至 7配合 initialNumToRender 的调整发现滑动体验并没有明显变差但内存占用下降了约 30%。这里要强调windowSize 不是越小越好。如果设置过小快速滚动时行来不及渲染会出现滚动到目标位置但内容空白的情况。我测试下来7 到 10 是一个比较稳妥的范围具体要看列表项的复杂度和图片数量。3. OpenHarmony 上 FlatList 的适配难点3.1 桥接层带来的额外开销RN 在安卓上走的是 JSI 或者旧的 Bridge 机制到了 OpenHarmony 上RN 框架的适配层通常是借助 OpenHarmony 的 ArkUI 组件来承载。也就是说RN 的视图最终要映射到 ArkUI 的组件树上这个中间多了一层转换。带来的直接问题就是每一次视图的创建、更新、删除都要经过 JavaScript 运行时到 ArkUI 的双向通信。列表滚动时行视图的频繁挂载和卸载会让这层通信变得非常繁忙。我在日志里统计过快速滑动一秒视图操作的消息量能达到几百条没做优化前这些消息的处理时长占了滚动总时长的 40% 以上。针对这个问题可行的优化方向有两个一是减少视图操作次数通过复用和回收而不是销毁重建来降低消息量二是合并消息把多次连续的视图操作批量提交。后者在 FlatList 的默认实现里没有直接暴露但你可以通过控制窗口大小和预渲染策略来间接实现。3.2 行组件层级对渲染性能的影响列表项组件层级越深在 ArkUI 上渲染的开销越大。这不是 OpenHarmony 独有的问题但它的影响比安卓更明显。我做了一次对比实验一个列表项包含三层嵌套 View另一个把嵌套压平为两层同时去掉不必要的父容器。在同样的数据量和滚动速度下压平后的列表滚动时的帧率从 38fps 提升到了 50fps 左右。这个提升非常可观。所以优化 FlatList 时不要去抠 item 内部某个样式属性的开销先看看组件树能不能瘦身。一个常见的做法是能用单一 View 配合 flex 布局解决的就不要包三层。特别是那些只做占位的容器删除掉对界面没有影响但渲染开销实打实减少了。3.3 图片加载对滚动的隐性影响列表性能杀手排名第一的永远不是文字而是图片。FlatList 虚拟化只能控制视图数量但控制不了图片解码和内存占用。在 OpenHarmony 上图片加载的链路跟安卓不同它最终走的是 ArkUI 的 Image 组件而 RN 的 Image 在映射到 ArkUI 时加载策略需要重新适配。我遇到的问题是列表快速滑动时图片加载请求大量并发导致磁盘 I/O 和内存峰值非常高滚动出现明显卡顿。后来我做了三件事第一在列表滚动时暂停加载非可视区域图片第二给图片加缓存策略重复出现的图直接命中缓存第三图片降采样原本 2000px 宽的图在列表场景只加载 400px 宽的版本。这三步做完滚动帧率稳定在了 55fps 以上内存峰值下降了约 25%。如果你在 OpenHarmony 上做列表优化图片这块一定要单独处理而且越早越好。4. 渲染管线层面的优化方向4.1 从同步渲染到分批渲染的调整FlatList 在快速滚动时新进入可视区的行会立即触发渲染。在安卓上这个渲染过程是异步的UI 线程和 JS 线程并行所以感知不明显。但在 OpenHarmony 的 RN 适配层视图提交的过程更接近同步操作JS 线程的一次 render 会直接阻塞后续的滚动事件处理。我实测的现象是快速滚动时偶尔出现卡住半秒然后跳一大截的情况就是 JS 线程被同步渲染卡住了。解决思路是让 FlatList 的更新不要全部挤在一帧内完成而是分批处理。具体做法是给 renderItem 的调用加一个队列每次处理固定数量的 item剩下的放到下一帧。这个改动在 JS 层就能做不依赖原生代码。具体实现我放到了后面实操环节这里先记住一个结论滚动流畅度的瓶颈往往不在渲染本身而在渲染是否阻塞了滚动的响应循环。4.2 getItemLayout 带来的收益FlatList 官方文档一直强调如果你的列表项高度是固定的一定要提供 getItemLayout。这个函数可以让滚动定位不做动态测量直接通过索引计算偏移量省掉大量的测量交互。在 OpenHarmony 上这个建议从性能优化变成了必做项。因为动态测量要等每个 item 渲染完成后才能拿到高度这个过程在 ArkUI 上比较慢而且频繁测量会带来额外的通信开销。我的列表项高度是固定的所以我写了 getItemLayout一行代码的改动滚动定位的耗时下降了 60% 以上。如果你用的是流式布局或者高度可变的卡片可以考虑把 item 的高度固定下来或者用最小高度约束来做近似测量收益同样明显。4.3 原生视图回收对内存稳定性的帮助FlatList 默认的行为是行离开可视窗口后卸载对应的原生视图。这个卸载在安卓上意味着资源释放但在 OpenHarmony 上频繁的卸载和重建会让内存产生碎片而且对象创建的开销不小。我尝试了一种回收复用的思路离开可视区的行不销毁而是回收到一个对象池里进入可视区时直接从池中取出复用。实现上需要重写 FlatList 的 cellRenderer 相关逻辑或者直接改造 VirtualizedList 的回收策略。这个改动的收益比较明显内存占用的波动从原来的不断攀升变成了一条平稳的曲线。GC 触发的频率也显著下降滚动过程中几乎没有因为 GC 导致的卡顿。5. 实操过程与关键配置5.1 基础参数调优清单如果你只想做最基础的优化不要动代码先从这几个参数开始调。这些参数都是 FlatList 直接暴露的 props改起来零风险。FlatList data{data} renderItem{renderItem} keyExtractor{keyExtractor} initialNumToRender{10} maxToRenderPerBatch{8} updateCellsBatchingPeriod{50} windowSize{10} removeClippedSubviews{true} getItemLayout{(data, index) ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, })} onEndReachedThreshold{0.5} onEndReached{loadMore} /这几个参数的含义我解释一下initialNumToRender首屏渲染的 item 数量。设得太大会拉长首屏时间太小会导致首屏下方空白。我一般取可视区能容纳的数量再加 2。maxToRenderPerBatch每批渲染的最大数量。这个值控制 JS 线程单帧的工作量设小一点更流畅。updateCellsBatchingPeriod批处理间隔单位是毫秒。间隔太短会让 JS 线程连续工作间隔太长会让滚动看起来跟手度下降。removeClippedSubviews删除被裁剪的视图。在 OpenHarmony 上这个属性需要用真机验证因为它依赖原生层正确报告裁剪区域。5.2 renderItem 稳定化的标准写法前面提到 renderItem 必须稳定这里给出一个标准的写法模板。核心是用 useCallback 保证函数引用不变再用 memo 包裹列表项组件。const renderItem useCallback(({ item, index }) { return ListItem item{item} index{index} /; }, []); const ListItem memo(({ item, index }) { // 你的 item 渲染逻辑 return ( View style{styles.item} FastImage source{{ uri: item.thumbnail }} / Text{item.title}/Text Text{item.subtitle}/Text /View ); });这里要特别提醒ListItem 内部的子组件如果用了内联对象或者内联函数memo 依然会失效。比如 style 如果是每次渲染新创建的对象那 memo 比较时发现 props 变了还是会重新渲染。解决方法是把静态样式提到组件外部用 StyleSheet.create 创建函数用 useCallback 包裹。我踩过一次坑ListItem 里有一个按钮onPress 直接写成了内联箭头函数memo 完全没用每个 item 在滚动时都会重新渲染。改成 useCallback 之后重新渲染的问题才解决。5.3 滚动暂停图片加载的实现图片加载对滚动性能的影响前面说过了这里给出一段可以抄的代码。核心思路是监听 FlatList 的 onScroll滚动开始时暂停非可视区图片加载滚动停止后再恢复。const [isScrolling, setIsScrolling] useState(false); const onScroll useCallback((e) { setIsScrolling(true); if (this.scrollTimer) clearTimeout(this.scrollTimer); this.scrollTimer setTimeout(() setIsScrolling(false), 100); }, []); FlatList onScroll{onScroll} scrollEventThrottle{16} // ... /配合上面的 isScrolling 状态你可以把需要渲染的图片源打一个延迟加载标记。比如用 react-native-fast-image 的话可以动态切换 source如果用的是普通 Image 组件可以先把 uri 设为空等滚动停止再赋值这样可以抑制滚动过程中的图片解码。需要注意这里的 isScrolling 状态变化会引起整个列表重新渲染所以不要直接把它放在上下文里传递而是用 ref 或者事件总线避免产生额外的渲染开销。5.4 分批渲染的具体写法分批渲染的目标是避免一帧内处理过多任务。我封装了一个简单的批处理队列function useBatchedRender(items, batchSize 8) { const [renderedCount, setRenderedCount] useState(Math.min(batchSize, items.length)); useEffect(() { let disposed false; let timer; if (renderedCount items.length) { timer setTimeout(() { if (!disposed) { setRenderedCount((prev) Math.min(prev batchSize, items.length)); } }, 50); } return () { disposed true; clearTimeout(timer); }; }, [renderedCount, items.length, batchSize]); return useMemo(() items.slice(0, renderedCount), [items, renderedCount]); }这个 hook 配合 FlatList 使用的方式是先用 useBatchedRender 截取数据长度再交给 FlatList 渲染。每次增加一批 item给 JS 线程留出空闲时间处理滚动事件。我测试下来这种慢一点点填充的方式反而比一次性渲染所有可见 item 更流畅因为滚动的首帧响应没有被长任务阻塞。5.5 如何验证优化效果优化不能凭感觉要打点验证。我用的工具组合是chrome devtools 的 Performance 面板看 JS 线程OpenHarmony 系统自带的进程内存统计看内存react-native 的 perf 接口看帧率。最有效的一个指标是 time to interactive after scroll。具体做法是从列表顶快速滑到底部记录最后一次滚动事件到画面完全稳定的时间。优化前这个时间基本在 1 秒以上优化后能压到 300ms 左右。如果测试时发现帧率还是不稳定优先看列表项的组件树深度和图片数量。这两个因素在 OpenHarmony 上的影响权重远大于 JS 逻辑本身的耗时。6. 常见问题与排查思路6.1 首屏出现白屏但数据已经返回这种情况下数据加载正常但 FlatList 首屏是空的。常见原因是 initialNumToRender 设置过小首屏可视区的行数超过了初始渲染数量导致部分行没有渲染。排查方法先看渲染的行数是否等于 initialNumToRender如果小于可视区行数加大这个值。另外检查 FlatList 的父容器是否有固定高度。如果没有固定高度FlatList 不知道自己的可视区域虚拟化会失效表现为所有行一次性渲染或者首屏空白。这个坑在 OpenHarmony 上更容易出现因为 RN 接入到页面里时外层有时会用 flex:1 撑开但父组件的测量时机和安卓不一致导致 FlatList 初始化时拿不到高度。6.2 快速滚动到底部出现短暂空白这个问题的根源是虚拟化窗口不够大。快速滚动时手指已经滑到目标位置但新行还在渲染队列里没有及时跟上。解决思路适当调大 windowSize 或者 initialNumToRender同时减少单个 item 的渲染耗时。如果你已经做了分批渲染可以把批处理间隔从 50ms 降到 30ms让新行的渲染更快。有一种情况是 getItemLayout 写错了 offset 计算导致滚动位置估算错误FlatList 认为目标行不在可视区也就不会调度渲染。检查方法是在不同位置暂停滚动打印当前可视区的起始索引对比实际界面内容误差超过一个 item 就说明计算有误。6.3 内存持续上涨滑动时间越长越卡这个问题通常不是 FlatList 本身导致的而是列表项的图片或子组件没有被正确释放。在 OpenHarmony 上RN 的 Native 组件销毁逻辑可能不会立即触发 ArkUI 的资源回收需要手动处理。我的做法是在 item 组件的 componentWillUnmount 里显式清空图片释放事件监听器。另外尽量不要在列表项里创建全局使用的动画或者定时器这些对象如果不手动清理会一直持有引用。如果你用的是 removeClippedSubviews要注意确认视图裁剪后的真实卸载情况。有时候行视图还在原生层保留着只是不可见这种情况下内存上涨是必然的考虑配合对象池方案来解决。6.4 某些 item 渲染时有闪帧或者跳动闪帧和跳动的原因通常是行高度没有严格一致。即使你的业务数据高度相同但如果 item 组件内部有图片加载导致的高度变化FlatList 在计算偏移量时会出错。解决方法给列表项设置一个固定的高度不要让内容撑开。比如图片加载前占位高度和加载后的高度保持一致。如果做不到严格固定至少给每个 item 外层 View 设置一个 minHeight避免加载过程出现布局抖动。另一种情况是 useCallback 的依赖问题。如果 renderItem 的 useCallback 依赖了外部状态比如一个计数器的值那这个函数引用仍然会变化导致所有 item 重新渲染。排查时把外部状态从组件中提出来放到全局 store或者用 ref 代替。7. 一些额外的心得和实测数据根据我个人经验FlatList 优化在 OpenHarmony 上的收益并不完全等同于在安卓上的收益。有些在安卓上可有可无的优化项在 OpenHarmony 上是决定性的。比如 getItemLayout在安卓上不做只是慢一点但在 OpenHarmony 上不做可能会出现明显的白屏感。实测下来一组综合优化的数据供参考优化前列表快速滚动帧率约 30fps内存峰值约 480MB优化后帧率提升到 56fps内存峰值降到 340MB。首屏渲染时间也从 1.8 秒缩短到了 1.1 秒。注意这些数据是特定机型和特定数据量下的结果不同场景会有差异但优化的方向是明确的。最后分享一个扩展思路如果你的列表项内部还有嵌套的滚动容器或者复杂的交互建议把这一类 item 从 FlatList 中拆出来用原生组件或者独立的页面承载。FlatList 的虚拟化机制擅长处理大量轻量且结构相似的行对于个别重量级的 item让它们脱离虚拟化列表反而更稳定。这个取舍在 OpenHarmony 上尤其值得考虑。
返回列表