React Native 跨平台代码复用率到底有多少?我把自己 12 万行的项目数了一遍

🔑 关键词:React Native,代码复用率,平台分叉,新架构,Hermes

📖 摘要:一个跑了三年的 RN 电商 App,124317 行业务代码里真正按平台分叉的只有 3.7%,但 217 处内联 Platform.OS 判断集中在 service 和 utils 层。这篇文章说清楚跨平台到底省了什么、没省什么,以及什么规模的团队才适合用 RN。

上周跟一个做 RN 的朋友吃饭,他问我:你们那个跑了两年的电商 App,到底有多少代码是两端共用的?我当时张口就来「90% 以上吧」,回来一数,脸有点疼。

图片

统计口径先说清楚:cloc 跑 src 目录,含 .ts/.tsx/.js,排除 node_modules、Pods、android/build、.generated.。结果是 124,317 行。然后我按文件名后缀筛出 .ios.tsx.ios.ts.android.tsx.android.ts,再加上 Platform.select 包出来的独立分支(这段得手动看,麻烦),大概 4,612 行,占 3.7%。

但我真正在意的不是这个百分比。我又跑了几条 grep:

grep -rn 'Platform.OS' src/ --include='*.ts*' | wc -l
# 217
grep -rn 'Platform.select' src/ --include='*.ts*' | wc -l
# 63
grep -rn 'NativeModules\.' src/ --include='*.ts*' | wc -l
# 88

图片

217 处内联判断。比那 4600 行分叉文件更让我头疼的就是这 217 处,因为它们不可见。你改一个 utils/formatPrice.ts,没人会提醒你里面第 88 行藏着一个 Platform.OS 判断。

这 217 处我数了下分布:services/ 67 处、utils/ 58 处、components/ 41 处、hooks/ 31 处、其他 20 处。注意 67 加 58 等于 125,超过一半落在非 UI 层,这才是真正难受的地方。

UI 层分叉其实无所谓。按钮在 iOS 上圆角 8、Android 上 4,这是设计规范要求的,改坏了顶多被设计吐槽两句。但 service 层分叉是另一回事。我们微信支付的封装 payService.ts 里,iOS 走的是自己写的原生 module(因为当年那个库不支持 universal link 回调),Android 走第三方库。两套路径、两张错误码映射表、两个超时策略。有次 Android 那边把「用户取消支付」的错误码从 -2 改成了 0(库作者升了个大版本),iOS 没动,结果订单状态机在 Android 上开始把取消当成功处理。从灰度到发现花了 6 个小时,损失多少单我不想回忆。

图片

原生依赖也是分叉重灾区。29 个带原生代码的依赖里,有 11 个在 iOS/Android 上的 API 签名不一样,你根本没法用一个 TS interface 包住。极光推送的 setBadge 两端参数类型都不同,高德定位的坐标系配置也不一样。这些东西你写文档都没用,只能靠人在 review 的时候记住。

到这里我想说一个跟主流说法不太一样的观点:跨平台框架真正省下来的不是代码量,是「发布同步成本」和「认知切换成本」。而这两样东西的价值跟团队规模强相关,跟代码复用率几乎无关。

算笔账。我们有 5 个客户端开发:2 个 iOS、2 个 Android、1 个 RN。如果改成纯原生,同样功能量大概要 8 个人。但反过来说,如果我们总共只有 2 个人,用 RN 就是纯亏——你得同时懂 Xcode 签名、Gradle 变体、CocoaPods 版本冲突、Android 混淆规则,然后还要写业务。

图片

所以我的判断标准是:客户端人数少于 3,别碰 RN,用 Expo 或者直接写原生都行;3 到 6 人是 RN 比较舒服的区间;超过 8 人,你又该考虑把核心页面拆回原生了。这话说出来可能会被踩,但我认。

还有个维度很少人提:iOS 和 Android 的发布节奏差异。我们 App Store 审核平均 26 小时,Google Play 一般 4 到 8 小时。以前两个团队各自发版,Android 修完 bug 当天就能上,iOS 等明天。现在一套代码,你反而要迁就慢的那个。这个损失是隐性的,但确实存在。

关于新架构,说点实际的。我们今年 3 月从 0.71 升到 0.74,开了 newArchEnabled=true。29 个原生依赖里 21 个支持 Fabric/TurboModule,剩下 8 个只能在老架构下跑,靠 interop layer 混着来。

图片

升级过程:pod install 第一次跑了 11 分钟,中途因为一个库的 podspec 里 s.platforms 写的是 ios 11.0,跟 RN 0.74 要求的 13.4 冲突,报了四十多行红字。改完 vendor 里的 podspec,下次 install 又被覆盖,最后只能 fork 那个库。这种坑任何官方文档里都不会写。

好处也有,数据是实测的:Hermes 加 Fabric 之后,我们拿 18 台低端机(红米 9A、三星 A03 这一类)跑首屏渲染,P50 从 1.42s 降到 0.97s,P90 从 3.1s 降到 2.2s。提升主要在长列表首次渲染,因为 Fabric 的 shadow tree 和 layout 挪到了 C++ 层,不用来回过桥。JS bundle 体积从 1.8MB 降到 1.6MB(开了 inline requires 加一部分代码分割)。

但如果你现在手里是个 0.63 的老项目,我的建议是别急。先把 Reanimated 升到 3.x,把动画都挪到 UI 线程,体验提升比新架构明显得多,成本还低一截。

图片

最后说个反常识的。用了三年 RN,我最大的收获不是「一套代码两端跑」,而是被迫读懂了 iOS 和 Android 两套平台的东西——因为出了事你得自己下到原生层去查。我现在看 Android 同事的 Kotlin 代码不费劲,看 Swift 也大概能懂。这个能力在纯 RN 团队里挺稀缺的。

如果你们团队在纠结要不要上 RN,我的建议是先拿一个二级页面(比如「我的」页或者设置页)做两周试点,别一上来就重构首页。试点里你一定会遇到:Metro 缓存、Podfile、Gradle 版本、第三方库兼容,这四个坑一个都跑不掉。踩完了再决定。

顺带一句,别忘了 npx react-native doctor,这命令救过我至少三次。

🏷️ 标签: