小程序开发两个星期后我为什么从uniapp换回了原生?

🔑 关键词:uniapp,小程序原生开发,跨端框架对比,小程序性能优化,taro

📖 摘要:一篇写于凌晨的真实复盘:我在同一台电脑上先后用uniapp和原生开发同一个小程序,记录下工具链、包体积、渲染性能、调试痛苦和最终选择。没有标准答案,只有取舍。

小程序开发两个星期后我为什么从uniapp换回了原生?

图片

起因特别简单:客户要求一个月上线一个带直播预约和分销功能的小商城。我团队里有人擅长vue,有人只会react,为了不写两套,我拍板用了uniapp。第一天很顺利,npm装个cli脚手架跑起来,H5端秒开,甚至觉得可视化编辑器是神器。但第三天起,问题像下水道反味一样冒出来:HBuilderX这货在macOS上编译到一半会莫名释放内存,大概4.6GB的node进程直接被杀。更恶心的是,微信开发者工具里的trace未捕获到错误,页面是白屏,但控制台只有一行JSON.stringify出错的提示,且这个错源自uniapp内部封装的encodeURIComponent对某些字符的处理——我根本没法断点进去。

图片

我们算了笔真实账:用uniapp写一个包含30个页面、2个自定义组件、接入了微信支付和云开发的电商demo,在微信开发者工具模拟器上首屏渲染时间平均是972ms,真机iPhone X上是1.4s,后台从onLoadfirstRender的日志时间戳差了一百多毫秒。而用原生微信小程序语法重写同样页面,没有使用Skyline而是老版webview渲染,首屏直接压到620ms,包体积从uniapp打出来的1.23MB(主包982KB)降到918KB(主包742KB)。注意,uniapp编译出来的代码里塞了大量它自己的运行时polyfill,而且每个页面js里都打了一套__$wrapper逻辑来处理生命周期和跨端API映射。这些你看不见的胶水层,在低端Android机上就是活生生的卡顿炸弹。

图片

有人会说,你选taro不就好了。真诚讲,我试了taro 4.0 beta,React生态确实爽,但那版本对微信小程序的wx.setClipboardData的promise化支持是残缺的,你需要自己强转,然后接口返回的filePath在特定iOS版本上会带file://前缀导致预览图片失败。社区issue里有人提,维护者回了一句“请更新到4.0.0-rc.5”,但rc.5又把另一个动画组件的class名编译丢了。跨端框架的宿命是:框架自己永远比平台慢半拍,而平台一更新,你就得烧香祈祷不要炸。微信2023年10月把scroll-view的滚降级属性改了,taro 3.x编译的代码里用了旧的bindscrolltolower写法,结果就是部分页面拉到最底部时加载事件触发两次,你一搜,全是自己在评论里骂。

我在项目第八天的时候决定写一个原生的小程序做对比。每天下班后专门用两小时,把同样的界面、同样的接口文档,用原生一行行写。感受很撕裂:原生没有数据流管理,复用逻辑得靠mixin或者自定义组件里的behavior,每写一次都要查文档,确实寂寞。但你获得了完全的控制感。我知道每个setData会经过什么序列化操作,知道renderer什么时候会走worker,知道requestAnimationFrame在小程序里只能用来做帧回调而没法做节流。这种掌控感,带来的是可以精确地避免用户提到的“手机发烫”问题。比如我们的直播预约日历组件,用跨端框架时要传递一个二维数组标记不可约日期,每次setData传12KB数据都要把整个数组重建一遍,渲染时间肉眼可见地掉帧。换原生后,我改成在onLoad里直接this.setData({ disabledMap: utils.buildMap() }),并且利用小程序的分包异步加载把逻辑拆进subpackage,连续滑动一周日期,帧率稳在55fps以上——uniapp甚至没给我机会测量,因为它的源码已经把你包在它自己的“数据diff里”。

图片

这不是一篇劝退跨端框架的文章。如果你只是做连续十几个静态页面展示产品、不需要复杂交互且对包体积要求放低到2MB内、团队都是vue出身、并且没时间维护两套,那uniapp依然能让你完成任务。但如果你做的业务涉及支付、直播、canvas后处理或虚拟列表,早晚会触碰平台底层。而且那些线上商家小程序出问题后,uni的客服会让你发个dcloud.log压缩包过去,三天后给你一个“需升级到最新版本”的回复。原生小程序你有完整的sourceMap和完整调用堆栈,只要懂点基础库源码,问题都能定位到自己代码。

图片

现在我这两套代码都还躺在同一个仓库里,一个叫app-uniapp/,一个叫app-native/。最后发给客户的是原生版。但我不会说原生是真理,只是它更接近这个平台的“物理现实”。开发工具、包管理、编译链,这一切都应当服务你思考产品本身,而不是让框架去弥补自己的漏洞。我前阵子读到微信团队关于渲染层和逻辑层分离演变的历史文章,才明白小程序的路子本就和浏览器web不一样,任何把小程序“当网页写”的框架,最后都是在替你建一堵假墙,然后你撞墙,再把墙修好。

图片

编辑器里还留着这段代码,原生版日历组件index.wxml的triggerEvent绑定,当时被uniapp多套了一层,很难查;原生在自定义组件里直接this.triggerEvent('select', { date: day }, { bubbles: true }),干净利落。如果你正纠结框架,拿真机测一测你自己场景里那个最卡的操作,看一分钟帧率数据,再做决定吧。