RNOH 页面滑动卡顿丢帧问题定位不迷路
本文面向 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 找到同一次滑动和异常帧
- 用滑动开始 trace、输入事件或视频对齐滑动区间;
- 读取该区间的目标刷新率和帧预算;
- 在 App Frame 和 RS Frame 中选择最差帧或最大连续丢帧区间;
- 记录绝对时间戳,并把后续证据统一换算为相对时间;
- 不要把不同次滑动的最低 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::performTransaction | transaction 调度入口 |
#RNOH::MountingManager::doMount | mutations 交给挂载实现 |
#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.cpp | onEmitOnScrollEvent、emitScrollEvent、sendEventForNativeAnimations |
| ArkUI Scroll | cpp/RNOH/arkui/ScrollNode.cpp | ON_DID_SCROLL 等事件入口 |
| Fabric 调度 | cpp/RNOH/SchedulerDelegate.cpp | performTransaction、telemetry marker、并行/切片路径 |
| native 挂载 | cpp/RNOH/MountingManagerCAPI.cpp | doMount、didMount、mutation 执行 |
| 预分配 | cpp/RNOH/ComponentInstanceProvider.cpp | onUITick 与帧截止保护 |
| 帧监控 | cpp/RNOH/Performance/JSFpsMonitor.cpp、ets/RNOH/RNPerfMonitorView.ets | 浮层指标及其实现限制 |
10. 相关文档
更多推荐



所有评论(0)