快手 KRN 鸿蒙适配与性能优化:从大规模迁移到接近原生体验

来源说明:本文根据快手动态化内核负责人张鹏的 CSDN 技术直播整理。内容不是逐字照录,而是在完整转写基础上重组技术主线、修正明显的语音识别错误。文中性能数字均来自直播分享,形如 [00:45:39] 的标记对应原视频时间点。

核心结论

一次迁移,真正困难的不是“让代码跑起来”

把一套 React Native 框架迁移到鸿蒙,如果只看技术名词,似乎无非是补齐平台接口、重写 UI 层、接入新的线程和事件机制。但快手 KRN 面对的并不是一个刚起步的框架,而是一套已经运行多年、承载海量业务的生产系统。

KRN 大约从 2020 年开始建设,基于 React Native 0.62 深度定制。它不仅提供 RN 内核,还补齐了容器与生命周期、Bundle 发布更新、API 与 Bridge、调试工具、数据采集和运营等完整的平台能力。[00:06:46]

直播披露的规模很能说明问题:

这组数字带来了两个硬约束:

  1. 上层业务接口和行为必须尽量稳定,一点变化都可能影响 2,000 多个 Bundle。
  2. 内核和平台层的变化必须能独立验证、灰度替换和安全回退,不能要求所有业务一起回归。

因此,KRN 鸿蒙适配的核心命题不是“能否迁移”,而是:

如何把变化收敛在平台底层,在不扰动上层业务的前提下完成迁移,并把性能重新拉回到接近原生的水平。

技术路线:把变量控制在平台层

项目在 2024 年底启动时,团队讨论过四条路线:[00:10:32]

  1. 鸿蒙端直接采用 RN 新架构,Android 和 iOS 保留旧架构;
  2. 把现有 KRN 旧架构内核迁移到鸿蒙;
  3. 等待 RN on OpenHarmony 官方提供旧架构支持;
  4. Android、iOS、鸿蒙三端同时升级 RN 新架构。

判断原则非常朴素:面对一个全新的操作系统,尽量少叠加变量。

只在鸿蒙端使用新架构,平台工作量会下降,但业务代码和内核会长期分叉;等待外部支持,时间和可行性无法控制;三端同时升级,则会把平台迁移、内核升级、近 2 万个内核修改和 2,000 多个 Bundle 的适配验证全部叠加,稳定性和节奏都难以管理。

团队最终选择了第二条路线:迁移现有 KRN 内核,把变化尽量压在平台底层。[00:13:06]

这不是理论上的最优解,而是在当时信息和约束下风险最可控的解。分享者也特别提到,如果项目启动时 ArkUI C API 已经成熟,路线选择可能会不同。

迁移拆解:接口稳定、内核重写、跨端逻辑复用

迁移工作同时从“功能模块”和“代码复用”两个维度拆解。

对业务可见的部分必须保持接口和行为一致,包括:

而依赖操作系统的内部实现必须在鸿蒙上重做,包括:

React Reconciler、Yoga、JS Runtime、JSI 等跨端逻辑可以复用或少量适配;ReactInstance、Package、NativeModule 注册管理等与 UI 无关的内核逻辑,可以按照语义迁移到 ArkTS;真正涉及 UI 和系统能力的部分则必须重新设计。[00:15:23]

为了避免项目迅速变成无法定位问题的黑盒,团队按工作流分层推进:

  1. 先跑通 JS 加载、执行和渲染指令生成;
  2. 再接 UI Manager、ShadowNode、Yoga、事件和最基础组件,打通最小渲染管线;
  3. 补齐 Bridge、生命周期、异常处理和更多组件;
  4. 最后完成调试、监控、异常上报和交付能力。

每一层都有明确输入、输出和测试条件,每次只引入一类新变量。最终,第一版 KRN 内核在一个季度多一点的时间里完成了从 Android 到鸿蒙的迁移。[00:17:29]

AI 在迁移中扮演什么角色

项目启动于 2024 年底,当时的模型上下文、Agent 能力和工具生态远不如现在。团队无法把整个模块直接交给 AI,只能把代码拆到函数级甚至更小的片段,让模型完成大量语法层面的转换,再由人工补齐:

AI 的价值是减少机械翻译工作,而不是替代架构判断。保障结果正确性的关键,仍然是 RN 内核单测、跨端 UI 树和属性对比,以及持续运行的自动化测试。[00:19:06]

命令式 RN 遇到声明式 ArkUI

RN 旧架构向原生层输出的是命令式操作,例如 CreateView、UpdateView、ManageChildren。在 Android 和 iOS 上,内核可以持有真实 View,随时创建、插入、删除和修改属性。

ArkUI 的声明式模型不同:组件由状态和数据描述,不能像 Android View 一样随时 newadd 和修改对象。项目早期又没有成熟的 ArkUI C API,因此 KRN 在 RN 与真实 ArkUI 组件之间增加了一层 ViewModel。[00:21:16]

ViewModel 保存:

UI Manager 继续接收 RN 命令,但操作的是 ViewModel;ArkUI 在自己的刷新周期中读取 ViewModel 并构建组件。这样既保留了 RN 旧架构的命令接口,也能通过 ArkUI 声明式语法创建和更新真实 UI。

这个中间层解决了兼容性,却引入了新的性能成本:

第一轮性能优化:精确更新与批量提交

第一版功能跑通后,团队首先解决两类直接问题。

用 FrameNode 缩小刷新范围

当时 ArkUI C API 尚未成熟,团队先使用 FrameNode 搭建了一层“ArkUI 命令式渲染层”。FrameNode 可以被创建、插入、删除,也能直接更新节点属性和控制布局刷新,因此变化可以限制在真正受影响的节点,不再触发整棵 ArkUI 树的大范围计算。[00:31:29]

代价是 FrameNode 的生命周期、调用顺序、异常回收和平台兼容性都需要 KRN 自己负责。它的主要收益不是单个 API 更快,而是减少隐式 Diff 和不必要的状态传播。

用 Dirty Set 合并一次提交内的多次变化

团队为连续到达的属性、布局和树结构操作增加了一个刷新窗口:

这相当于在渲染管线中增加一次批处理:不改变业务结果,但减少中间状态和重复计算。[00:33:24]

这轮优化后,华为测试中的不达标项从 53 项降到 25 项,首屏整体耗时下降约 30%。但性能仍未完全达标,因为更深层的瓶颈来自 ArkTS 执行效率、ArkUI 节点更新成本,以及布局和测量仍占用 UI 线程。[00:35:40]

第二轮性能优化:核心渲染管线迁移到 C++

2025 年第二季度,ArkUI C API 已经可用。团队没有简单地“把所有组件改成 C API”,而是选择了一条可渐进迁移的混合路线:

这种路线支持按组件灰度迁移和快速回退,但也让内核在相当长时间内维护两套实现,节点管理复杂度显著上升。

混合树最棘手的是事件,而不只是渲染

渲染链路从 React 指向 Native,事件链路却从 UI 反向回到 JS。C API 与 ArkUI 有不同的事件来源、数据结构、Target 表示和生命周期。

团队因此增加 EventAdapter,统一事件结构、目标节点和内容,再交还 React 前端处理冒泡、拦截和冲突。同时验证四类一致性:

  1. 节点插入、删除和重排;
  2. Yoga 布局结果;
  3. 事件顺序和语义;
  4. 创建、复用、销毁和异常回收等生命周期。

所有相关模块均提供开关,可在 C++/C API 与 ArkTS 版本间灰度切换和回退。第一版内核迁移只用了一个季度多一点,而 C++ 核心渲染管线从设计到落地用了约三个季度,足见其风险和复杂度。[00:40:10]

C API 改造后的数据

直播公布了几组结果:[00:45:39]

指标结果
高端机启动耗时相比 ArkTS 版改善约 42%,相比 Android 快约 10%
低端机启动耗时相比 ArkTS 版改善约 52%,相对 Android 也有进一步提升
复杂列表卡顿率(高端机)下降约 92%
复杂列表卡顿率(低端机)下降约 43%

但 C API 并没有消灭所有瓶颈。简单页面的平均 FPS 可以明显领先 Android,而复杂电商长列表仍落后:高端机约低 23%,低端机约低 32%。原因不是最终绘制,而是复杂页面会触发更多布局和 Bridge 调用;在混合架构下,这些计算仍可能把 UI 线程打满。[00:46:44]

后续方向包括把带自定义 ShadowNode 的组件优先 C API 化、把布局线程迁出 UI 线程、将可迁移的高频 Bridge 放到 C++ 或 Worker 线程,以及继续研究并行化创建和更新 C API 节点。

三端通用的性能方法:先建立分段指标

性能优化不能只盯一个总耗时。KRN 把页面加载拆成多个阶段:[00:52:25]

分段后,性能回退可以被定位到容器、JS、网络、渲染还是资源加载,而不是只看到一个总数字。

T0:并行化冷启动任务

T0 中的插件、动态库、线程、JS Runtime、基础包和业务包缺一不可,因此优化重点不是删步骤,而是减少计算量和串行等待。

KRN 的做法包括:

V8 Snapshot:把基础包执行从秒级降到几十毫秒

React Reconciler 和 React Native 等基础库体积大,低端机上的解析、编译和执行可达到秒级。KRN 使用 V8 Snapshot,把执行基础包后的堆、全局变量、函数、对象和引用关系序列化,冷启动时直接恢复。

难点在于 RN 会向 JS 注入大量 Native 引用。函数地址、对象地址和外部资源不能直接固化,恢复时必须通过 External Reference 重建,并保证数量、顺序和语义与生成 Snapshot 时完全一致。Native 接口变化时,Snapshot 也必须同步失效或重建。[00:58:19]

T1:Code Cache、Tree Shaking 和延后非关键模块

业务 Bundle 越大,JS 引擎在首屏前需要解析和编译的代码越多。KRN 的优化包括:

全量 Code Cache 并不总是更好:它降低运行时编译,却会增加产物、IO、反序列化、内存峰值和 OOM 风险。平台需要根据 Bundle 大小,在“预编译收益”和“缓存加载成本”间做选择。[01:02:44]

T2:预请求不是“提前发请求”那么简单

RN 的业务请求往往要等 Bundle 编译、全局代码执行、骨架屏渲染之后才发出。KRN 在 Native 触发页面加载时就构造请求,让网络与容器、引擎和 Bundle 初始化并行;JS 真正发起请求时,再与预请求做严格参数匹配,命中则复用回包,否则安全回退。

直播给出的经验数据是:

在 2,000 多个 Bundle 的规模下,请求参数并不是静态配置。参数可能来自页面 URL、容器数据、登录状态、Bridge 返回值,多个请求之间还可能有依赖关系。

因此 KRN 为预请求实现了一个小型表达式引擎:配置随 Bundle 下发,被解析成 AST,支持取值、类型转换、条件分支、递归求值和格式处理。它的真正目标是:在 JS 尚未启动时构造出与最终请求完全一致的数据,并在任何不确定条件下自动回退。[01:11:26]

T3、FMP 和资源阶段

后续阶段还有三类代表性优化:

内存优化:先找到真正的大头

直播以 Android 为例拆解了一个 KRN 引擎的内存构成。首次引擎大约占 41 MB,其中约 19 MB 是可被系统换出的代码页等 Clean 内存,约 22 MB 是 V8 Heap、ShadowNode、UI 实例等 Dirty 内存。框架内核自身只占几 MB,盯着内核代码做极限压缩,收益并不大。[01:18:24]

KRN 的框架侧内存优化集中在四个方向:

  1. 共享 JS Runtime:单个引擎首次约 40 MB,后续通常还增加 10~20 MB。一个页面存在多个 KRN 容器时,共享 Runtime 是最大的内存杠杆,但必须额外处理全局变量隔离、异常恢复、生命周期和资源释放。
  2. 治理大块资源:图片按实际展示尺寸压缩,规范动画和 Graphics 资源的缓存、复用和释放。
  3. 收敛生命周期:页面退出及时释放容器和 Runtime;暂时不可见时触发 GC 和缩堆。
  4. 建设复合监控:联合 Native、JS Runtime、对象数量和资源信息定位问题。[01:21:00]

监控体系:从“线程卡了”追到“哪段业务代码”

Native APM 可以发现 CPU、内存或 UI 线程异常,却不知道是哪个 RN 页面、哪个 Bundle、哪段 JS 代码造成的。KRN 为此分层补充监控:

若要采集 JS 堆栈,必须在引擎自己的执行线程中完成。V8 可通过 RequestInterrupt 让引擎在安全上下文执行回调,再读取当前堆栈;JavaScriptCore 没有同类中断 API,需要借助最长执行时间回调,并构造 Error 获得堆栈。[01:25:35]

这套体系的目标,是把“某线程在某时刻卡住”继续推进到“哪个业务 Bundle 的哪段代码在做什么”。

长列表白屏:异步内容生产与同步 UI 消费的冲突

RN 长列表快速滑动时出现白屏,并不一定是代码 Bug,而是两种机制的天然冲突:

当 UI 已经滑到下一个 Cell,而 JS 还没有完成渲染时,只能在“阻塞 UI”和“先显示空白”之间选择。RN 为减少卡顿,通常选择后者。[01:29:10]

一次失败但有价值的尝试

团队曾在每个原生 Cell 中放置独立 RootView,并让 JS 异步渲染。实际结果是:

这次失败证明原生 Cell 复用有价值,但不能以双重状态管理为代价。[01:31:22]

同步渲染管线:异步生产、同步读取

重新设计后的管线保留 JS 和 Shadow 层的原有职责,只修改 UI 提交:

  1. 对标记为“需要同步渲染”的子树,拦截原有 CreateView、UpdateView 等 UI 命令;
  2. 从 ShadowTree 提取节点位置、属性、类型和层级;
  3. 通过 Diff 组织成版本化 UI 数据;
  4. 使用 Copy-on-Write 发布新版本;
  5. JS/Shadow 是唯一写入方,UI 线程只读,不反向修改状态。

这样,UI 可以随时读取一份完整的旧版本数据,不必等待 JS 或 Shadow 线程,也不会出现空白;新版本准备好后再原子切换。代价是用户可能短暂看到旧内容,但 RN 本身就是异步渲染,这不会进一步放大原有延迟。[01:34:07]

Copy-on-Write 的关键价值是:

更新时先做 Cell 级 Diff,控制列表刷新范围;再在 Cell 内按 View 类型、Tag、属性、布局和子节点递归比较。结构兼容就复用真实 View 并更新属性,不兼容才删除重建。[01:37:46]

这套同步渲染机制不只用于长列表,也适用于任何要求当前帧立即获得可展示内容的场景。原有 RN 组件无须修改,业务仍使用相同的嵌套和事件模型。

最后的经验:平台稳定,底层可演进

整场分享可以归纳为三条主线:

  1. 迁移:上层接口和行为保持稳定,把变化封装在平台边界之内;
  2. 性能:建立分段指标、优化手段和监控定位的完整闭环;
  3. 体验:必要时改造渲染机制,而不只是对单个函数做微优化。

KRN 的最终目标不是单纯追求跨端复用率,也不是把每个平台都强行做成同一种实现,而是:

保持跨端开发效率,同时让用户体验尽可能接近原生。[01:43:54]

这也是大规模跨端基础设施最值得借鉴的一点:上层稳定不意味着底层静止。真正成熟的平台,应该允许底层持续重写、灰度、验证和回退,而业务不必为每次演进重新付费。