本文面向 RNOH 应用开发者,说明如何从 HarmonyOS 系统帧异常出发,沿 RNOH 滚动、JS 更新、Fabric 挂载、ArkUI 绘制和 Render Service 合成链路定位页面滑动卡顿。

在开始定位前,应先按照RNOH 框架性能问题定界确认问题属于应用侧,并区分 RN 业务、原生业务、三方组件和 RNOH 框架。响应时延与完成时延问题见响应时延与完成时延性能分析。

1. 问题与指标

1.1 典型现象

  • 手指拖动或惯性滚动时不连续、瞬时停顿;
  • 吸顶、分页或列表项更新时出现明显跳变;
  • 快速滑动时出现白块、空卡片,随后内容补齐;
  • 滚动联动动画落后于手势;
  • 平均 FPS 尚可,但存在明显连续丢帧。

白块不一定等于丢帧。页面可能按时产帧,但数据、图片或列表项尚未填充。应同时检查帧泳道和内容更新时间。

1.2 帧预算

单帧预算(ms) = 1000 / 问题窗口内的目标刷新率(Hz)

60 Hz、90 Hz、120 Hz 的理论预算约为 16.7 ms、11.1 ms、8.3 ms。设备可能动态切换刷新率,定位时应从 Display、H:PreferredFrameRate 或等价系统信息读取实际目标,不能固定按 60 Hz 判断。

优先关注:

  • App Frame 和 RS Frame 中的异常帧;
  • 卡顿率/累计丢帧时间;
  • 最大连续丢帧;
  • Present Fence 或实际显示帧间隔;
  • 同一异常窗口内应用主线程、RNOH_JS 和 Render Service 的状态。

平均 FPS 只适合描述整体趋势,不能代替对最差帧和连续丢帧的分析。

2. 定位前准备

  • 使用 release 构建和 release bundle,关闭 Metro、HMR、Inspector、高频日志和性能浮层后再正式采集。
  • 固定设备、系统/RNOH 版本、屏幕刷新率策略、数据、网络和操作方式。
  • 预热页面后重复相同滑动至少 5 次,并保留正常与异常样本。
  • 在业务侧为滑动开始、关键数据更新或吸顶切换添加 trace 点,方便与系统帧对齐。
  • 使用 DevEco Profiler Frame/Time 工具采集 App Frame、RS Frame、调度和调用栈。只有原始 .htrace 时也应保留原文件;可导出数据库时优先使用数据库做可复核查询。

HarmonyOS 的 Frame 分析界面和字段会随 DevEco Studio 版本变化,具体操作以ArkUI 页面滑动卡顿丢帧问题分析为准。

3. 总体定位流程

确认滑动窗口与目标刷新率
  → 用 App Frame / RS Frame 确认真正的异常帧
  → 判断应用侧还是 Render Service 侧先异常
  → 检查应用主线程、RNOH_JS 的运行/等待状态
  → 用 RNOH/Fabric/ArkUI 调用栈还原该帧关键路径
  → 用功能开关、负载缩放和版本 A/B 验证根因

3.1 找到同一次滑动和异常帧

  1. 用滑动开始 trace、输入事件或视频对齐滑动区间;
  2. 读取该区间的目标刷新率和帧预算;
  3. 在 App Frame 和 RS Frame 中选择最差帧或最大连续丢帧区间;
  4. 记录绝对时间戳,并把后续证据统一换算为相对时间;
  5. 不要把不同次滑动的最低 FPS 和最长任务拼到一起。

3.2 先区分应用侧和系统侧

帧表现优先方向
App Frame 异常、RS Frame 正常应用未按时产帧,检查应用主线程、RNOH_JS、Fabric 和 ArkUI
App Frame 正常、RS Frame 异常Render Service、GPU、图层合成或系统资源竞争
两侧均异常比较最早异常点;应用晚提交可向 RS 传导,RS 阻塞也可能反压应用
两侧均正常但有白块检查数据到达、图片解码、列表填充和业务占位策略

只有应用侧先异常,才继续按 RNOH 应用链路分析。

3.3 判断线程是运行、抢占还是等待

对异常帧窗口检查 sched_slice、线程状态和调用栈:

  • 持续 Running:CPU 上确实执行了长任务,沿调用栈找热点;
  • 长时间 Runnable:线程想运行但未获调度,检查整机竞争、QoS 和温控;
  • 休眠/阻塞:检查锁、同步 TurboModule、跨线程等待或 I/O;
  • 调用栈很短但帧仍晚:继续检查 ArkUI、Render Service、GPU 和前序依赖。

线程忙和 FPS 下降只构成相关性。热点必须覆盖异常帧截止点或推迟该帧所需输入,才能视为因果证据。

4. RNOH 滑动链路

4.1 原生滚动与 JS 事件

RNOH ScrollView 的主要链路为:

ArkUI ON_DID_SCROLL
  → ScrollNode / ScrollViewComponentInstance
  → 读取 offset、速度和容器信息
  → 按 scrollEventThrottle 与位移变化决定是否向 JS 分发 scroll
  → 原生 Animated 旁路处理滚动事件
  → 需要时由 React 产生新的 commit/diff/layout
  → mutations 在主线程挂载
  → ArkUI 布局/绘制 → Render Service

ArkUI 自身可继续滚动,不要求 JS 每帧都返回结果。因此 RNOH_JS 忙并不必然造成纯原生滚动丢帧;只有滚动视觉效果依赖 JS 计算、列表补项或 React 更新时,JS 延迟才会进入关键路径。

4.2 scrollEventThrottle 的当前实现

ScrollViewComponentInstance::onEmitOnScrollEvent() 在以下任一条件成立时向 JS 事件管线分发 scroll:

  • m_allowNextScrollEvent;
  • 已接近 scrollTo 目标;
  • 达到 scrollEventThrottle 间隔并发生显著位移。

当前实现中 scrollEventThrottle == 0 会放开时间间隔门控,但 dispatchUniqueEvent 仍会合并同一 event beat 内的重复事件。状态回写 updateStateWithContentOffset 另有约 51 ms 的节流,这与业务 onScroll 回调频率不是同一个概念。

不要机械地把 scrollEventThrottle 固定为 16 ms:在 120 Hz 屏幕上它约等于每两帧一次。应根据业务是否需要逐帧 JS 回调、回调耗时和目标刷新率实测。只用于动画联动时,优先使用支持的原生驱动能力,减少 JS 进入逐帧关键路径。

4.3 Fabric 与挂载

重点 trace 包括:

trace/marker含义
FABRIC_COMMIT_*React/Fabric commit 时间
FABRIC_DIFF_*ShadowTree diff 时间
FABRIC_LAYOUT_*Fabric layout 时间
#RNOH::SchedulerDelegate::performTransactiontransaction 调度入口
#RNOH::MountingManager::doMountmutations 交给挂载实现
#RNOH::MountingManager::didMount <count>batch 执行及数量线索
FABRIC_BATCH_EXECUTION_*mutations batch 的 native 执行区间

FABRIC_UPDATE_UI_MAIN_THREAD_* 只包围 synchronouslyUpdateViewOnUIThread,不能用来代表所有 transaction 的主线程挂载时长。

如果 commit/diff/layout 很长,优先检查 JS 更新和组件树规模;如果这些阶段按时完成但 didMount 或 ArkUI measure/layout 超预算,优先检查 mutations 数量、组件创建成本和原生布局。

5. 常见根因与验证方法

5.1 onScroll 或 JS 长任务过重

证据:

  • 异常帧前 RNOH_JS 长时间 Running,或滚动事件持续排队;
  • JS 任务结束后才出现 Fabric commit 和可见更新;
  • 关闭 onScroll 业务逻辑后,异常帧和 JS 排队同时消失。

处理:

  • onScroll 内只保留必须的轻量逻辑;埋点、预取等移到合适的滚动阶段或异步任务;
  • 避免每个事件创建大对象、解析大 JSON 或触发多次状态更新;
  • 根据实测调节 scrollEventThrottle;
  • 滚动联动动画在属性受支持时使用原生驱动。

5.2 React 重渲染和提交过频

证据:

  • 每个或多个 scroll 事件后紧跟 commit/diff/layout;
  • React Profiler 显示大量无效重渲染;
  • 稳定 props 或关闭滚动状态更新后,commit 数量和丢帧同步下降。

处理:

  • 避免把无需渲染的数据写入 React state;
  • 使用 React.memo、稳定的 callback/style/数据引用;
  • 合并状态更新,缩小依赖滚动位置的组件范围;
  • 将 renderItem 与页面其他状态解耦。

5.3 列表虚拟化或补项不及时

证据:

  • 白块对应列表项尚未创建,而 App/RS Frame 可能仍正常;
  • Fabric batch 中 create/insert 数量集中,随后 ArkUI measure/layout 增长;
  • 缩小 windowSize、简化列表项或提供稳定布局信息后明显改善。

处理:

  • 按场景调节 initialNumToRender、maxToRenderPerBatch、windowSize 等参数;
  • 固定尺寸列表项可提供 getItemLayout,但动态高度场景不要伪造错误尺寸;
  • removeClippedSubviews 能减少离屏 ArkUI 节点参与绘制,但可能带来重新挂载或复杂变换下的显示风险,应 A/B 验证后使用;
  • 降低单个列表项的组件层级、图片尺寸和同步初始化成本。

5.4 native 挂载或 ArkUI 布局超预算

证据:

  • Fabric 已按时产出,异常窗口主线程集中在 didMount、组件创建、属性更新、CreateTaskMeasure、CreateTaskLayout 或 FlushTask;
  • mutations 或 ArkUI 节点数量与耗时同步增长;
  • 将数据量减半后耗时近似下降。

处理:

  • 避免一次 transaction 创建过多节点;
  • 拆分非首屏内容,减少动态测量和深层布局;
  • 检查自定义组件的创建、属性设置和布局实现;
  • 对框架并行创建、预分配和切片是否生效做 trace 验证,不要只检查编译开关。

5.5 FlashList 或三方列表组件

当前 RNOH 会识别 AutoLayoutView 和 CellContainer,并对相关 Surface 采用串行保障路径,以避免复用和重排被并行创建破坏。串行本身不是缺陷;只有它位于异常帧关键路径且同负载下存在不合理回归,才进入框架问题分析。

验证时应:

  • 确认组件版本与当前 React Native/RNOH 版本兼容;
  • 与相同数据规模的 FlatList 或最小列表做对照;
  • 检查列表项创建成本,而不是只把 estimatedItemSize 缺失作为根因;
  • 确认业务没有自定义同名 AutoLayoutView/CellContainer 触发特殊路径。

5.6 图片、GC 和系统资源竞争

证据:

  • 图片 decode/upload 与白块或异常帧重合;
  • RNOH_JS 在 GC 窗口停止推进,JS VM/PSS 在此前快速增长;
  • 应用主线程长时间 Runnable 而非 Running,CPU/温度或其他进程负载异常;
  • App 按时产帧但 RS Frame 异常。

这些情况不能仅凭 RNOH 线程存在活动就归因框架。应分别处理图片规格和缓存、JS 对象分配、整机负载、温控或 Render Service 问题。

6. 白块专项判定

白块定位应回答“壳、数据、文本、图片分别何时到达”:

现象优先检查
卡片壳已出现,文本和图片都晚数据更新、JS render、列表补项和挂载时机
文本已出现,只有图片晚网络、解码、缓存、图片上传
节点已经挂载但画面未更新ArkUI flush、RS 合成和图层提交
白块期间 App/RS Frame 均正常内容准备问题,不应表述为“丢帧”

不要仅凭一个长 FlushTask 声称它导致白块;必须证明内容树在需要提交的帧截止前尚未完成。也不要在缺少失败或异常 trace 时声称模板、资源或组件“加载失败”。

7. 根因报告示例

现象:120 Hz 目标刷新率下,第二次惯性滚动出现 4 个连续异常 App Frame。
锚点:滑动开始 ts=...;异常区间为 +186 ms~+231 ms。

证据:
1. App Frame 先异常,RS Frame 在同窗口正常,定界到应用产帧侧;
2. RNOH_JS 从 +174 ms 到 +219 ms 持续执行 onScroll 触发的业务计算;
3. FABRIC_COMMIT_START 在该任务结束后才出现,首个相关 batch 错过帧截止;
4. 关闭 onScroll 中的数据排序后,JS 长任务、晚提交和连续丢帧同时消失。

结论:RN 业务 onScroll 长任务导致提交延迟,不是 RNOH 框架或 Render Service 问题。

8. 排查清单

  • release 构建、日志和调试工具状态符合测试要求
  • 已记录问题窗口实际目标刷新率和单帧预算
  • 已确认异常 App/RS Frame,而不只看平均 FPS
  • 已用同一个锚点对齐帧、线程状态和调用栈
  • 已判断主线程/RNOH_JS 是 Running、Runnable 还是阻塞
  • 已证明热点位于异常帧关键路径
  • 已检查 JS 回调、React 提交、mutations、ArkUI 和 RS 全链路
  • 白块已区分帧异常与内容准备延迟
  • 已完成业务功能开关、负载缩放或版本 A/B
  • 结论包含排除项,未把相关性写成因果性

9. 关键代码索引

以下路径均位于 packages/tester/harmony/react_native_openharmony/src/main/:

模块文件关注点
滚动状态与事件cpp/RNOHCorePackage/ComponentInstances/ScrollViewComponentInstance.cpponEmitOnScrollEvent、emitScrollEvent、sendEventForNativeAnimations
ArkUI Scrollcpp/RNOH/arkui/ScrollNode.cppON_DID_SCROLL 等事件入口
Fabric 调度cpp/RNOH/SchedulerDelegate.cppperformTransaction、telemetry marker、并行/切片路径
native 挂载cpp/RNOH/MountingManagerCAPI.cppdoMount、didMount、mutation 执行
预分配cpp/RNOH/ComponentInstanceProvider.cpponUITick 与帧截止保护
帧监控cpp/RNOH/Performance/JSFpsMonitor.cpp、ets/RNOH/RNPerfMonitorView.ets浮层指标及其实现限制

10. 相关文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐