先甩结论:uni-app 鸿蒙端调 ArkTS 原生插件,参数能用对象直传就别用 JSON 字符串裹一层——我在一台 Mate 60 Pro 上跑了 20 组,5000 条小数据直接传对象比字符串方案快了 43% 左右。但你别高兴太早,数据一过 2MB,对象桥的序列化成本直接炸,那时候字符串方案反而更稳。下面把两次 benchmark 和根因都摊开说。
我当初上原生插件,是为了给一个长列表瀑布流做原生渲染加速。官方 List 渲染上万条数据在低端机上掉帧掉得我心里发毛,滑两下就白屏一下,用户吐槽说"像在翻老式相册"。我试过懒加载、cachedCount 调参、组件复用,效果是好了点但没根治,干脆把列表的排版和每条案例的收益计算整个挪到 ArkTS 插件里做,uni-app 只负责把数据喂过去。问题就出在"喂数据"这一步——这玩意儿看着不起眼,坑全藏在桥的序列化里,文档一个字都没讲明白。我前前后后在这上面卡了差不多一周,从怀疑网络、怀疑插件线程,一路排到桥的序列化,才知道真正的瓶颈根本不在我想的那几个地方。
最早我图省事,JS 侧先 JSON.stringify 裹一层丢给插件,插件再 JSON.parse 回来处理,处理完又 stringify 回 JS 层。整条链路跑了两趟序列化,纯属脱裤子放屁。当时我还自我感觉良好,觉得"字符串最稳,不会有类型对不上的幺蛾子"。说白了就是偷懒,懒得去研究桥到底怎么搬对象。
uni-app 侧两种写法长这样:
// pages/case-list/case-list.vue —— uni-app 侧两种传参写法const radarPlugin = uni.requireNativePlugin('RadarCasePlugin')// 方案 A:JSON 字符串裹一层function loadByString(payload) {const t0 = Date.now()const str = JSON.stringify(payload) // 出参序列化const res = radarPlugin.loadCasesFromString(str)const list = JSON.parse(res) // 入参反序列化console.log('string 方案耗时', Date.now() - t0, 'ms')return list}// 方案 B:直接传对象,交给桥自动 marshalingfunction loadByObject(payload) {const t0 = Date.now()const list = radarPlugin.loadCases(payload) // 桥把 JS 对象转成 ArkTS 对象console.log('object 方案耗时', Date.now() - t0, 'ms')return list}
插件侧两个方法,一个吃字符串自己解,一个直接吃对象:
// uts/RadarCasePlugin.ets —— ArkTS 原生插件侧export class RadarCasePlugin {// 方案 A:接收字符串,自己 JSON.parseloadCasesFromString(raw: string): string {const list = JSON.parse(raw) as Array<CaseItem>const out = this.enrich(list)return JSON.stringify(out) // 再字符串回去给 JS 层}// 方案 B:接收结构化对象,桥已经转好 ArkTS 类型loadCases(raw: Array<CaseItem>): Array<CaseItem> {return this.enrich(raw)}private enrich(list: Array<CaseItem>): Array<CaseItem> {return list.map(it => ({ ...it, score: it.monthlyProfit / it.invest }))}}
第一次 benchmark,数据量是 5000 条小记录,单条约 80 字节,总共 400KB 出头。跑出来的数(20 组均值,单位 ms):
小数据下,object 方案省掉的就是那两趟 JSON.parse/stringify 的冤枉路。说实话我一开始还不信,自己手测了三遍才认——这买卖怎么算都亏,早该直接传对象。我甚至有点后悔,前面那两个项目也都傻乎乎地全程 stringify,白白多烧了不少无意义的毫秒。那会儿我还发朋友圈吐槽自己"用最贵的写法做最没必要的搬运"。后来我把这套对比贴到项目群里,组里另一个同事说他之前也踩过,只不过他那边是图片列表、体量大,所以一开始就用的字符串,反而没我这么纠结。
我用下面这个脚本各跑了 20 组取均值,确认数字不是手抖:
// 计时脚本:两种方案各跑 20 组取均值function measure(fn) { const t = Date.now(); fn(); return Date.now() - t }function avg(xs) { return xs.reduce((s, n) => s + n, 0) / xs.length }async function bench(payload) {const a = [], b = []for (let i = 0; i < 20; i++) {a.push(measure(() => loadByString(payload)))b.push(measure(() => loadByObject(payload)))}return { stringAvg: avg(a), objectAvg: avg(b) }}
我承认上一节说得太满。把数据量加到 2 万条,每条再带一段 base64 缩略图,总大小干到 2.3MB,再跑一遍,结果直接反过来:
object 方案慢了快一倍。我当时盯着控制台的日志愣了十秒,脑子里第一反应是"桥抽风了"。用上面那个脚本各跑了 20 组,两次都卡在 1600ms 往上,确认不是偶发。你猜怎么着,我把 base64 缩略图去掉、只留纯文本字段,object 方案立刻又比字符串快了。说明那堵墙就立在"数据体量大"这一个点,跟字段内容无关。这个发现让我把之前写的几个接口又翻出来复查了一遍,果然有两个大数据接口悄悄踩了坑。
根因在鸿蒙那套 uts 桥的机制上。对象直传时,桥要在 JS 值和 ArkTS 值之间做 deep copy marshaling,把每一层都重新建成 ArkTS 对象。打个比方,字符串方案像是把一摞文件装进一个箱子搬过去,桥只搬箱子;对象方案像是把每份文件拆开、逐页复印成鸿蒙格式再搬,页越多复印越慢。小对象这步代价可以忽略,但 2MB 的嵌套结构一进来,CPU 全耗在反复建对象上,比你自己 JSON.parse 还狠。字符串方案反而只占一份连续内存,桥只搬一个 string,开销是线性的,所以到了大数据量级,字符串方案反倒更扛造。我后来用 hilog 打了时间戳,确认耗时几乎全部发生在桥的 marshaling 阶段,插件里的 enrich 计算本身只占零头。也就是说,对象越大,你付给桥的过路费越高,这费还不是固定一笔,是跟着数据量往上猛窜。
顺便吐槽一句,uni-app 文档对桥的序列化机制语焉不详,只在某个角落提了句"建议小数据直传",压根没说大数据的墙在哪、墙多高。我这是自己撞出来的,官方那行字跟没说一样。真要较真,这套 marshaling 的开销曲线其实值得单独写一篇,但目前连个官方性能基准都找不到。
阈值我卡在 1.5MB 左右,这个数不是拍脑袋。我在 0.5MB、1MB、1.5MB、3MB 四个点都跑过:1MB 以内 object 稳赢,1.5MB 两边都差不多,一过 2MB object 就开始翻车,3MB 时 object 已经比 string 慢两倍多。超过阈值就别直传对象了,两条路:
还是用 JSON 字符串,稳,就是慢点; 更狠一点,把数据落盘成文件,只把路径传给插件,让 ArkTS 侧用鸿蒙 fs 直接读,彻底绕开桥的序列化。
// 超过阈值时,把数据落盘,只传路径async function loadHuge(payload) {const path = `{Date.now()}.json`await writeFile(path, JSON.stringify(payload))return radarPlugin.loadCasesFromFile(path) // ArkTS 侧直接 fs 读}
// ArkTS 侧:从文件读,不经过 JS 桥的序列化loadCasesFromFile(path: string): Array<CaseItem> {const raw = fs.readText(path) // 用鸿蒙 fs 读,绕开桥return this.enrich(JSON.parse(raw))}
我现在的写法是按 size 动态选:小于 1.5MB 走 object,大于就走文件路径。雷达鸭鸿蒙版那个案例瀑布流就是这么干的,5000 条一条不差。封装层里我先 JSON.stringify 量一下长度,再决定走哪条分支,调用方完全无感。
反正从那以后,我但凡在 uni-app 鸿蒙端调原生插件,第一反应不再是"先 stringify 保平安",而是先看这坨数据多大。如果让我重来,我会一开始就把 size 判断写进封装层,而不是等到线上掉帧、用户吐槽才回头补这堂课。说句心里话,这套桥的 marshaling 开销曲线挺反直觉的——小数据省事又快的写法,大数据下恰恰是最慢的。你那边如果也卡在插件传参的性能上,先量一把 size 再决定走哪条路,别像我一样傻跑两趟字符串才反应过来。
我是老三,10+ 年软件开发经验,软件设计师、人工智能应用工程师,平时主要折腾鸿蒙 ArkTS 北向开发加 Web 前端,也在摸索 AI 自动化。偶尔在 CSDN 写点鸿蒙和 AI 方向的实战笔记。
本文遵循 MIT 协议,转载请注明出处。
夜雨聆风