拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

在OpenHarmony上用React Native开发收藏页:选型、桥接与性能优化全记录

在OpenHarmony上用React Native开发收藏页:选型、桥接与性能优化全记录

上个月接到一个需求:把团队现有的动漫社区应用 AnimeHub 移植到 OpenHarmony 设备上,第一个要交付的页面就是收藏页面。这个页面看起来不复杂——展示用户收藏过的动漫列表,支持取消收藏、下拉刷新、点击进详情页。但真正动手之后才发现,RN for OpenHarmony 这六个字背后,藏着一整条链路的问题:选型、环境、桥接、性能、生态差异,每一步都可能翻车。

这篇文章我就把自己做 AnimeHub 收藏页面这段实战经历完整记录下来,包括为什么选 React Native 而不是 ArkTS 原生,收藏页面的需求到底该怎么拆,RN 工程怎么跑上 OpenHarmony,数据层和 UI 层怎么落地,以及真机调试时踩到的坑。如果你正准备在 OpenHarmony 上做 RN 开发,或者单纯好奇这条技术路线现在到底能不能用,这篇应该能帮你省不少时间。

1. 选型复盘:为什么是RN而不是ArkTS原生开发

1.1 OpenHarmony应用的三种主流开发路线

先说结论:在 OpenHarmony 上做应用,目前主要就三条路——ArkTS/ArkUI 原生、Flutter、React Native。每条路背后都有一批人在踩坑,也都有各自适合的场景,但很多人一开始就陷入了"sologan 之争":原生党说跨平台性能差,跨平台党说原生开发慢。实际做项目的时候,这个争论毫无意义,你只需要看自己的约束条件。

ArkTS/ArkUI 是 OpenHarmony 官方主推的原生方案,性能确实最好,平台能力也是最新的,菜单、弹窗、安全控件这些系统能力都是第一时间支持。但问题是,ArkTS 这语言太新了,团队里没几个人写过,从零开始学再加业务开发,工期根本压不住。而且你如果有 Web 端或者安卓端的存量业务,ArkTS 一套代码完全没法复用。

Flutter 在 OpenHarmony 上也有社区适配,渲染性能很能打,Dart 语言也相对好上手。但 Flutter 的 OpenHarmony 适配版本滞后比较明显,很多插件都要自己改,遇到平台能力缺口的时候,排查问题的难度比 RN 更高。

React Native 这条路最初我是犹豫的,因为它在安卓和 iOS 上的老架构性能一直被诟病。但后来我仔细盘了一下自家团队的情况:前端工程师占大头,React 技术栈非常熟,公司还有一套跑在安卓上的 RN 业务代码可以抽取复用。RN for OpenHarmony 虽然不是官方出品,但社区迭代速度肉眼可见,已经有完整的组件映射和桥接方案。综合下来,RN 是这三条路里最能"用现有团队、保现有资产、控交付周期"的路线。

1.2 RN for OpenHarmony 的生态现状摸底

决定走 RN 之后,我先花了两天时间把社区适配情况摸了个底。目前主流的适配项目是 react-native-openharmony,它通过 OpenHarmony 侧的组件映射和桥接层,把 RN 的 JS 运行时、渲染指令和原生组件翻译成鸿蒙的 ArkUI 组件。也就是说,你写的<View>最后会映射成 ArkUI 的Column或者Stack,<Text>映射成Text,<Image>映射成Image。

需要冷静看待的是它的版本跟进速度。RN 官方现在都推到 0.78 了,但 OpenHarmony 适配版还停留在 0.72 左右的基线,新架构 Fabric 的适配也只在早期验证阶段。这意味着你在 npm 上看到的最新版 RN 组件,很多都没法直接用,得在 0.72 这个生态里找兼容版本。

不过对我这个 AnimeHub 收藏页面来说,这个限制影响不大。我要用的核心组件无非就是 View、Text、Image、FlatList、TouchableOpacity,这些都是最基础的映射组件,适配成熟度已经很高,不用整天跟原生层缠斗。真正要小心的是一些需要原生能力支撑的三方库,比如图片缓存、网络请求、本地存储,这部分我后面单独讲。

1.3 当时做的技术决策清单

为了让自己事后不后悔,我把决策依据写成了清单,这里直接分享给你参考:

  • 团队技术栈:前端 8 人,有 5 人熟练 React/RN,ArkTS 只有 1 人看过文档。选 RN 意味着全员可以立刻进入开发状态,选原生意味着要先花 3-4 周培训。
  • 业务复用:AnimeHub 已经有 Web 端管理后台,收藏列表的数据结构、图片裁剪规则、分页协议都是现成的,RN 可以直接复用相同的接口设计。
  • 页面性能要求:收藏页是一个典型的长列表页面,不涉及高频刷新的复杂动画。RN 的 FlatList 在优化到位的情况下,完全能保障每秒 60 帧的滚动体验。
  • 交付周期:整个 AnimeHub 项目第一版要求 6 周内出可演示的 Demo,收藏页面只是其中一个模块。RN 的开发效率在这里是决定性优势。
  • 生态风险:RN for OpenHarmony 目前还不支持新架构的完整能力,所以我把"新架构落地"标记为长期观察项,而不是当前阻塞项。

2. 需求拆解:收藏页面到底要解决哪些问题

2.1 收藏场景的业务闭环分析

收藏页面的第一个坑,就是把它当成一个单纯列表页来做。实际上,收藏是一个典型的业务闭环,入口和出口都比表面上多。用户从动漫详情页点击"收藏"按钮,数据写入收藏列表;然后这个列表被带到收藏页面展示;用户在这里再次点击"取消收藏",数据要从列表移除。这个闭环里还夹着登录态、收藏数统计、服务端同步等问题。

我在 AnimeHub 里把收藏闭环拆成了四个环节:收藏动作(详情页写入)、数据存储(本地和服务端双写)、列表呈现(收藏页拉取和渲染)、取消动作(用户操作和状态回写)。每个环节都有自己的异常场景:写入失败怎么办?服务端同步失败本地要不要保留?取消收藏时接口超时怎么提示?

如果不把这个闭环想清楚,收藏页很容易做成"只读死列表",连基本的操作反馈都没法保证。

2.2 交互细节:用户真正在意的是什么

做开发的人往往盯着功能能不能跑通,但用户在意的是手感。我访谈了团队里几个重度二次元用户,总结出收藏页最在意的四个交互点:

  • 点击卡片任意区域都能进详情,但收藏按钮区域不能误触跳转,必须做事件拦截。
  • 取消收藏要有即时反馈,按钮状态从"已收藏"变回"未收藏",最好配合一个缩出的动画,让用户明确感知到"操作生效了"。
  • 空状态不能是一片白板,要有插画、文案和跳转引导,把用户从"我什么都没有"的低落情绪里拉出来。
  • 下拉刷新不要重置滚动位置,用户看到第 30 条的时候刷新一下,结果跳回顶部,谁都会崩溃。

这些细节看着不起眼,但决定了这个页面到底是一个"能用"的列表,还是一个"好用"的收藏中心。

2.3 数据模型设计与详情页联动

数据模型是整个页面的地基。我在 AnimeHub 里定义了这样的结构:

interface AnimeItem { id: string; title: string; coverUrl: string; lastEpisode: number; // 用户看到的最新一集 totalEpisodes: number; // 动漫总集数 tags: string[]; // 类型标签,如"热血""科幻" isFavorite: boolean; // 收藏状态 updatedAt: number; // 服务端最近更新时间,用于排序 }

这里有个关键决策:lastEpisode必须单独存,不能每次从详情页现查。因为收藏列表要展示"你追到第几集了"这个信息,如果用户详情页只看了一集,回到列表页它必须立刻反映出来,不能等重新拉接口。这个字段会让收藏页和详情页产生联动:详情页更新了观看进度,收藏页下次进入时要同步显示。

分页参数也在这个环节定下来,我用的是基于游标的分页协议:每页 20 条,响应里带有nextCursor,没有nextCursor就代表没有下一页了。相比传统的page/pageSize,游标分页在收藏数据频繁增删的时候更稳,不会因为新插入的数据导致翻页重复或遗漏。

3. 环境搭建与工程联调:让RN跑在开源鸿蒙上

3.1 项目初始化与依赖版本选择

先说版本。RN for OpenHarmony 的适配版本和 RN 官方版本是绑定的,我用的组合是:

{ "react-native": "0.72.15", "react-native-openharmony": "0.72.5", "@react-native-async-storage/async-storage": "1.19.3", "react-native-gesture-handler": "2.12.1" }

这里要特别强调:不要直接安装最新版的 RN。我见过有人用npx react-native init直接拉了一个 0.76 的工程,然后去配 OpenHarmony 适配包,结果依赖冲突排了一下午。正确做法是先看 react-native-openharmony 官方仓库的 version mapping 表,它明确写了支持哪个 RN 版本,然后按那个版本初始化工程。

初始化完成之后,按照仓库的集成文档,把 OpenHarmony 原生工程放到harmony子目录下,同时配置metro.config.js让 Metro 打包器能找到鸿蒙侧的入口文件。这个入口文件路径和安卓的不一样,我一开始漏配了,Metro 直接报Unable to resolve module @react-native-openharmony/turboModule,排查了很久才发现是路径匹配的顺序问题。

3.2 桥接层基础:JS侧与OHOS原生模块的通信逻辑

说一个很多初学者不理解的概念:为什么 RN 在 OpenHarmony 上跑,还需要一个"桥接层"?因为 RN 的 JS 引擎本身没有能力直接调用 OpenHarmony 的系统 API,比如读取本地文件、获取设备信息、弹一个系统的 Toast。这些能力都在 OHOS 原生侧,JS 侧必须通过桥接模块发一个消息过去,原生侧处理完再回调结果。这个模型和安卓上的"Bridge"本质上是一样的。

在 react-native-openharmony 里,桥接模块用 ArkTS 写在原生工程里。比如我写了一个获取设备型号的模块:

import { TurboModule } from '@ohos/ReactNativeBridge'; export class DeviceInfoModule extends TurboModule { getDeviceModel(): string { return deviceInfo.marketName; } }

JS 侧调用的时候:

import { TurboModuleRegistry } from 'react-native'; const DeviceInfo = TurboModuleRegistry.get('DeviceInfoModule'); const model = DeviceInfo.getDeviceModel();

理解了这个机制,后面遇到"某个 RN 原生模块在 OHOS 上不能用"的时候,你就知道应对思路了:自己写一个桥接模块,把系统能力暴露给 JS。收藏页面里"拨打电话"和"读取本地缓存"就是这么干的,后面踩坑章具体讲。

3.3 一次成功的构建过程示范

搭建环境最容易卡壳的就是构建。我记录了一次完整成功的构建链路,你可以照着走:

  1. 安装 DevEco Studio并配置 OpenHarmony SDK,我用的是 API 10 的 SDK。
  2. 把harmony原生工程导入 DevEco Studio,这个工程在项目初始化/集成步骤里生成,不要手动新建。
  3. 配置entry模块的module.json5,把包名、版本号和应用图标按实际项目改掉。
  4. 配置签名。真机调试必须签名,模拟器调试可以跳过。我一开始图省事没配签名,结果 HAP 包装不上真机,报错信息又很隐晦,浪费了半小时。
  5. 执行 hvigor 构建。DevEco Studio 里直接点运行按钮即可,它会自动编译原生侧、打包 HAP、然后通过 hdc(OpenHarmony 的调试工具,类似 adb)安装到设备。
  6. 启动 Metro 服务。在项目根目录执行npm start,Metro 会监听 8081 端口,提供 JS bundle 给设备加载。
  7. 在真机上确认 Metro 连接。进入 App 后,开发者菜单里检查 bundle 来源是不是你本机 IP:8081。第一次连接必须确认这一步,否则页面渲染不出来,大概率是网络或来源配置问题。

我建议把以上步骤保存成一个BUILD.md文档,因为团队成员各自搭建环境时,卡的点五花八门,但基本都是这里面的某一环节。

4. 数据层落地:收藏列表的存储、分页与状态同步

4.1 本地持久化选型与封装

收藏数据需要本地持久化,原因很朴素:如果用户断网打开 App,他至少还能看到上次缓存的收藏列表,而不是一个空白页。RN 生态里最常用的持久化方案是 AsyncStorage,但它在 OpenHarmony 上不是开箱即用的。

我踩过的坑是:@react-native-async-storage/async-storage这个包虽然声明支持 OpenHarmony,但我在真机上测试时发现,部分版本在系统重启后数据会丢失,怀疑是底层实现没有正确调用 Preferences 的 flush 机制。后来我果断换方案:自己写一个桥接模块,底层用 OpenHarmony 的 Preferences 存储 API。这个库是同步落盘的,可靠性比 AsyncStorage 的异步写入更稳。

封装后的调用方式很简单,JS 侧不需要关心底层逻辑:

// storageHelper.ts import { NativeModules } from 'react-native'; const StorageBridge = NativeModules.OHOSStorageBridge; export const StorageHelper = { async getItem(key: string): Promise<string | null> { return StorageBridge.getItem(key); }, async setItem(key: string, value: string): Promise<void> { return StorageBridge.setItem(key, value); }, async removeItem(key: string): Promise<void> { return StorageBridge.remove(key); }, };

4.2 分页拉取与"加载中/加载失败"状态机

收藏列表要有分页拉取,但真正重要的是把状态定义清楚。我见过太多人只在 state 里放一个data数组,然后loading一个布尔值,结果一遇到加载失败就卡在"没有提示也没有重试"的死角。

我在 AnimeHub 收藏页里定义了完整的状态机:

type FetchState = | { status: 'idle' } | { status: 'loading'; isInitial: boolean } | { status: 'success'; data: AnimeItem[]; nextCursor: string | null } | { status: 'error'; message: string; retryable: boolean };

isInitial字段用来区分首屏加载和分页加载:首屏加载显示全屏 Loading,分页加载只在列表底部显示一个"加载中"的行。retryable字段决定错误提示是"重试"还是"返回上一页"。这个状态机写清楚之后,后续任何 UI 联动都变得非常简单——只需要根据status分支渲染对应组件。

分页参数我会在每次请求时带上cursor,服务端返回nextCursor。收藏数据本身是高频变动的,用户上一秒取消收藏,下一秒服务端列表就变了,用游标分页可以避免分页错位。

4.3 取消收藏的乐观更新与失败回滚

取消收藏这个动作,常见的做法是等接口返回成功后再更新列表。但这在弱网环境下体验很差,用户点了取消,转圈两秒,才有反应,而且要是接口挂了,用户以为取消了,其实没有。

我采用乐观更新:点击按钮的瞬间,先把本地列表里该项的isFavorite置为 false,并从列表中移除,然后发起接口请求。请求成功就完事;请求失败,就把该项插回原位,同时弹 Toast 提示"取消失败,请检查网络"。

代码结构大概是这样:

const removeFavorite = useCallback(async (item: AnimeItem) => { // 记录原位置,用于失败回滚 const prevData = dataRef.current; const removedIndex = prevData.findIndex((d) => d.id === item.id); // 乐观更新 updateData((d) => d.filter((it) => it.id !== item.id)); try { await api.removeFavorite(item.id); } catch (err) { updateData((d) => { const next = [...d]; next.splice(removedIndex, 0, item); return next; }); Toast.show('取消失败,请检查网络'); } }, []);

关键点是"记录原位置",因为分页列表可能已经滚动过了,直接插回数组末尾位置就不对了。用removedIndex能保证数据回到它原来所在的位置,视觉上不会产生跳跃感。

5. UI实现:把收藏卡片做出"手感"

5.1 卡片布局的关键取舍

收藏卡片我用的是单列大卡布局,而不是双列瀑布流。原因很直接:收藏列表要展示的信息量比普通内容流大得多。卡片上要有封面、标题、追更进度、标签、最近更新时间。双列模式下,这些信息挤在一起,用户阅读效率反而下降。

单列卡片的竖向长度我控制在 120 物理像素左右,封面尺寸固定为 84x112(3:4 比例),标题最多两行截断,进度信息放在卡片底部,整个视觉重心保持稳定。这里有个细节:封面图的裁剪模式用resizeMode="cover",并且封面容器隐藏溢出部分,这样服务端返回的图片无论什么尺寸,都不会撑破卡片布局。

5.2 FlatList列表性能配置细节

收藏列表理论上可以上千条,用 ScrollView 渲染是灾难,必须用 FlatList。但默认配置的 FlatList 在低端 OpenHarmony 设备上滚动还是会卡顿,需要针对性地调参。

我最终采用的配置是:

<FlatList data={favorites} keyExtractor={(item) => item.id} renderItem={renderCard} getItemLayout={(_, index) => ({ length: CARD_HEIGHT, offset: CARD_HEIGHT * index, index, })} initialNumToRender={6} maxToRenderPerBatch={8} windowSize={7} removeClippedSubviews onEndReachedThreshold={0.3} onEndReached={loadMore} ListEmptyComponent={EmptyState} ListFooterComponent={FooterLoading} showsVerticalScrollIndicator={false} />

逐项说下理由:getItemLayout最关键,它让 FlatList 可以直接计算每个 item 的位置,省去动态测量,滚动跳转性能提升非常明显。initialNumToRender={6}控制首屏渲染数量,避免一次性渲染 20 条导致首帧卡顿。maxToRenderPerBatch={8}控制每帧最多渲染 8 个新 item,避免长列表滚到底部时一次性渲染大量组件造成掉帧。windowSize={7}是渲染窗口的高度倍数,过大会浪费内存,过小会导致快速滚动时白屏,7 是实践下来的平衡值。

5.3 空状态与骨架屏的取舍

很多团队喜欢一上来就做骨架屏,但骨架屏只对"快速渲染内容"的场景有意义。收藏页面的首屏数据如果 200ms 内能返回,骨架屏一闪而过,用户根本注意不到,反而浪费开发工时。我最终只做了全屏 Loading 和空状态两个互补组件。

空状态组件我花的心思最多。收藏是用户主动积累的内容,如果收藏列表为空,用户情绪通常是"我怎么没有收藏东西",这时文案要正向引导,而不是冷冰冰一句"暂无数据"。我用的文案是"喜欢的番剧还没收藏哦",配一个手绘风格的看板娘插画,下面一个"去发现好番"按钮。这个按钮结合了收藏页面的深层需求——把用户带回推荐流,重新激活收藏业务闭环。

5.4 交互动效实现路径

取消收藏的动效,我用的是 RN 内置的Animated库,没有引入额外的动画框架。原因很简单:一个缩移动效而已,引入 reanimated 还要处理新架构兼容问题,在 OpenHarmony 上风险更高。

动效逻辑是:用户点击取消按钮时,卡片先做一次透明度淡出和缩放(从 1 缩到 0.85),同时卡片 X 轴向右偏移 24 个像素,然后才真正从数据源中移除。动画时长 150ms,不长不短,用户能感知到"操作生效"但不会觉得拖沓。

const scale = useRef(new Animated.Value(1)).current; const handleRemove = () => { Animated.parallel([ Animated.timing(scale, { toValue: 0.85, duration: 150, useNativeDriver: true }), Animated.timing(translateX, { toValue: 24, duration: 150, useNativeDriver: true }), ]).start(() => { removeFavorite(item); }); };

这里有个细节:动画用useNativeDriver: true,把动画执行放到原生侧,JS 侧只发一次指令,性能远好于 JS 驱动动画。OpenHarmony 上 RN 对useNativeDriver的支持已经比较稳定,可以放心用。

6. 真机调试踩坑实录:OHOS独占的那些问题

6.1 图片加载被拦:网络安全配置的坑

第一个让我抓狂的坑是图片加载。模拟器上封面图渲染得好好的,一上真机全部变成空白图,控制台也没有报错。我一开始以为是图片地址的问题,单独在 WebView 里打开那个 URL,又能正常显示。

排查过程是这样的:先确认接口请求有没有发出去,在 Metro 面板看到网络请求是发了的,说明不是接口层的问题。接着怀疑是 RN 的 Image 组件映射异常,把图片换成本地资源测试,本地图片能显示,说明映射没坏。最后才想到是不是网络安全策略的问题——我用的封面图地址是http://开头,OpenHarmony 默认禁止明文流量。在安卓上这个策略叫 cleartext traffic,OpenHarmony 也有类似机制,需要在module.json5里配置网络安全策略。

解决方案是在entry/src/main/resources/base/profile/network_config.json里放行明文流量,或者更稳妥的办法:把所有封面图换成 HTTPS 地址。我最终选择的是强制 HTTPS,因为配置明文流量放开等于降低了全 App 的安全性,不值得为图片加载妥协。

6.2 拨打电话功能的桥接差异

收藏列表里有一个"电联催更"的入口,用户点击可以直接拨打客服电话。这个需求在安卓上写起来很轻松,RN 的Linking模块直接调Linking.openURL('tel:10086')就行。但在 OpenHarmony 上,RN 适配版的Linking模块对tel:协议支持不完全,调用后毫无反应,也没有报错。

我当时的排查思路是:先用Linking.canOpenURL('tel:10086')检测系统是否能处理这个协议,返回 false,说明系统层面不支持。这就不是 RN 的问题了,而是 OHOS 没有把tel:协议映射到电话应用。解决办法只能自己写桥接模块,调用系统 API 打开拨号界面。

ArkTS 侧的核心代码:

import { featureAbility } from '@ohos.abilityFeatureAbility'; import { wantConstant } from '@ohos.abilityWantConstant'; export function dialPhone(phoneNumber: string): void { const want = { action: wantConstant.Action.dial, uri: `tel:${phoneNumber}`, parameters: {}, }; featureAbility.startAbility(want); }

JS 侧通过自建桥接模块调用,效果和原生一致。这个坑给我的教训是:在 OpenHarmony 上做 RN 开发,任何依赖系统能力的功能都要先在真机验证一遍,不能默认"安卓怎么跑这里就怎么跑"。

6.3 本地文件资源与FTP场景的适配

AnimeHub 的运营后台会把番剧封面和预告片素材同步到云存储,但也有一些素材是放在内部资源服务器上的,比如局域网内的 FTP 服务。收藏页有一步"下载缓存封面到本地"的需求,涉及到从 FTP 访问文件的场景。

OpenHarmony 访问本地文件或者 FTP 资源时,权限模型和安卓不太一样。你需要先在module.json5里声明ohos.permission.READ_MEDIA或ohos.permission.INTERNET等权限,而且运行时还要动态弹窗授权,不能在后台静默访问存储空间。

另外,如果图片组件要加载 FTP 或者自定义协议的资源,RN 的 Image 组件默认不支持ftp://这类的 URI,不能直接塞进 source 里。正确做法是先把远端文件下载到应用沙箱目录,再把本地文件路径交给 Image 渲染。我封装了一个downloadFileToCache(url)的工具函数,底层用 OHOS 的@ohos.request模块下载,完了返回本地路径。

6.4 热更新与调试端口问题

最后一个高频坑是调试连接。RN 开发最爽的是改代码后 Ctrl+S 页面自动刷新,这个能力依赖 Metro 和设备的局域网连接。但 OpenHarmony 真机的网络环境比安卓复杂,设备连的 Wi-Fi 和开发机不在同一个网段时,Metro 的localhost:8081根本访问不到。

我的习惯是启动 Metro 时强制指定 host:

npx react-native start --host 0.0.0.0

同时在设备的开发者菜单里手动设置 Debug server host 为开发机的局域网 IP(形如192.168.x.x:8081)。如果设备无法直连开发机网络,还有一个备选方案:通过 hdc 做端口映射,把设备的 8081 端口映射到开发机的 8081 端口,这样设备上的 App 就可以通过localhost:8081访问 Metro 了。

这个映射写法是:

hdc fport tcp:8081 tcp:8081

做完映射后再确认设备上 App 的 Debug 地址设为localhost:8081。热更新卡住、页面转菊花的时候,九成是这层网络问题没打通。

7. 性能优化:结合RN新老架构谈OHOS现状

7.1 老架构在OpenHarmony上的瓶颈

谈到性能优化,绕不开 RN 的新老架构对比。老架构(也就是 RN 0.72 及之前的版本)的核心问题是所有 JS 和原生通信都要经过 Bridge 消息队列,走 JSON 序列化,异步传递。你以为调用了一个原生模块,实际上这条消息要先被序列化,跨过 Bridge,再被反序列化,原生执行完,再走同样的流程回来。这个过程在低端 OpenHarmony 设备上的延迟比较明显,尤其是滚动列表中频繁触发图片加载、事件上报这种高频操作,掉帧是常态。

OpenHarmony 适配版目前基本还是基于老架构的桥接模型,所以我做收藏页时对 Bridge 调用非常克制。能合并的原生调用就合并,比如存储读取我封装成批量接口,避免每次滚动都触发一次原生异步调用。

7.2 新架构Fabric适配进度与选择建议

新架构(Fabric + JSI + TurboModule)是对老架构的彻底重构。它的核心变化有两个:一是 JS 可以直接通过 JSI 同步调用原生方法,不再需要 JSON 序列化和异步桥;二是渲染管线从 Shadow Tree 变成事件驱动的同步渲染,布局和提交效率大幅提升。直观效果是启动速度更快、列表滚动更流畅、原生模块调用开销趋近于零。

但 OpenHarmony 适配版的新架构还处于早期验证阶段,社区主线版本都还没把 Fabric 完整跑通。我当时的评估是:生产环境继续用老架构,新架构作为技术调研跟进。如果项目对列表性能有极端要求,优先通过优化 FlatList 配置、图片缓存和减少 Bridge 调用来解决,而不是铤而走险切换新架构。

7.3 列表流畅度的几项实测优化

最后列一下我在 AnimeHub 收藏页上实测有效的几项优化,这些都是直接改在代码里的:

  • 图片缓存:收藏列表封面的图片缓存是最大性能提升点。OpenHarmony 上默认的 Image 组件不带磁盘缓存,每次滚动到某个 item 都要重新下载图片。我给 Image source 包了一层自研缓存组件,下载完成后复用本地路径,实测滚动掉帧率从 45% 降到 12%。
  • React.memo 包裹卡片组件:收藏列表的数据经常更新(比如取消收藏、进度变化),如果列表里 500 个 item 全都需要重渲染,那性能就废了。用React.memo包住卡片,配合稳定的 props 引用,数据更新时只有真正变化的 item 会重渲染。
  • 避免匿名函数传递:renderItem里不要直接写onPress={() => ...},每次都创建新函数,会让 memo 失效。我把点击处理写成useCallback包裹的稳定函数,再传给子组件。
  • onEndReached防重复触发:分页加载时,onEndReached在边界位置很容易连续触发多次。我在loadMore里加了if (isFetching || !nextCursor) return的守卫,避免重复请求。

最终在 OpenHarmony 真机(设备是四核 A55,内存 4GB)上测试,收藏列表 500 条数据,快速滚动帧率稳定在 50-60fps,满足上线标准。说实话,这个表现比我预期的好。很多说"RN 在 OpenHarmony 上不能用"的人,大概率是没做性能优化就直接下结论了。

这个项目做下来,我的体会是:RN for OpenHarmony 这路目前确实蛮荒,很多能力要靠自己造轮子,但核心渲染链路和基础组件的成熟度已经能支撑真实业务了。收藏页面只是 AnimeHub 的第一块拼图,后面还有播放页、个人中心、评论区,每一步翻车留下的经验,都会成为下一次重建时的垫脚石。

返回列表