AI 时代写代码变便宜了,编译等待、磁盘、依赖和验证成了新瓶颈。我把同一个音乐播放器用 Rust/Tauri 和仓颉/cj-tauri 各实现一遍,前端一字不改,同口径测编译、增量重编、体积、内存、启动与功能,附全部脚本与没测到的部分。AI 时代的桌面应用开发:Rust/Tauri 与仓颉/cj-tauri,同一个播放器全量实测。
测评案例项目地址:
- Rust听歌应用项目:https://atomgit.com/qq8864/rust-music
- cj-tauri听歌应用项目:https://atomgit.com/qq8864/cj-tauri/tree/main/examples/music
前言:AI 时代,为什么还值得亲手比一遍
现在写一个桌面小工具,"让 AI 写"往往不是最难的一步。难的是它写完之后的事:等编译、等依赖下载、看本机还剩多少磁盘、以及确认它是不是真的能跑起来。AI 把写代码这一步的成本压低了,这些成本反而变得更刺眼——因为你现在会更频繁地改、更快地试,一天里真正耗在等待上的时间被放大了。
桌面应用这个方向尤其明显。前端界面基本是一次生成、之后小改;后端全是胶水代码:调接口、转图片、解析歌词、写本地文件。这种活对 AI 最友好——有成熟生态的语言,AI 能照着现成答案写;生态薄的语言,AI 就只能靠猜,你回头还得逐条验证。
所以"AI 时代该用哪门语言写桌面应用"这个问题,绕不开三件很朴素的事:这门语言的生态够不够 AI 抄、编译快不快、机器扛不扛得住。被叫"神器"的框架这些年出现过不少,我只报我自己量到的数。
于是我做了这个实验:把 cj-tauri 仓库里自带的 examples/music(一个网易云榜单播放器)用 Rust + Tauri 2 重写一遍,前端一个字都不改,同一台机器、同一个后端、同一套脚本,把两边各测一遍。
我不打算写成"谁赢"。这两套东西的差别不在口号上,而在你要付出什么、换回什么——下面所有数字都附了复现命令,你能自己跑一遍;没测到的地方我也单独列一节,免得看着像什么都测了。
先说结论
我把 cj-tauri 仓库里自带的 examples/music(一个网易云榜单播放器)用 Rust + Tauri 2 重写了一遍,前端一个字没改,然后在这台机器上把两边都用同一套脚本测了一遍。
测下来的结论有点反直觉,所以先摆在这儿:
- 编译效率:仓颉赢了,而且不是小幅领先——冷构建 21 秒对 11 分 09 秒;改一行代码再编,也是 12 秒对 50 秒。
- 磁盘占用:这一项仓颉赢得干净——中间产物 12–16 MB 对 871 MB–1.2 GB(55–75 倍);环境本身也是仓颉小,但只有 2.4 倍(2.2 GB 对 0.82 GB,删掉 rust-docs 后缩到 1.2 倍),不是数量级。项目越多,中间产物那一项拉得越开。
- 单文件体积:Rust 13.08 MB(strip 后 9.66 MB),仓颉 7.36 MB(strip 后 6.09 MB)。但这 6.09 MB 有个前提——它把 std/stdx 当系统库,另外还要带 44 个运行时动态库、合计 15.19 MB。所以真按净分发体积算,Rust 的 9.66 MB 反而最小,仓颉 21.28 MB。如果把仓颉改成静态链接 stdx,主二进制涨到 9.46 MB、外部库只剩 4 个 4.76 MB,合计 14.22 MB——两种配置我都量了,下面分开写。
- 启动速度:基本打平,首屏数据到达 1.80 秒对 1.99 秒。
- 内存:两边都在两三百兆(全树 PSS,同环境)。仓颉版高约 15%(308 MB 对 267 MB),且多出来的几乎全在它自己的宿主进程上;这一项的主导变量其实是 WebKit 合成开关——开合成两边一起涨到 458 / 484 MB。谁要是说「差 200 MB」,那是把各进程 RSS 相加了。
- 功能完整度:两边同一套 10 项自检,都是 10 passed / 0 failed。
也就是说,"Rust 慢、体积大"和"仓颉跑得快"这两句话,在编译和运行这两个阶段恰好是相反的。而且选哪边,取决于你被什么卡住:如果硬约束是磁盘和迭代速度,答案会直接落到仓颉;如果约束是生态、单文件交付、招人和资料存量,Rust 依然更稳。我自己这台机器就是前者——剩不到 10 GB 磁盘,一个 Rust 桌面工程就要吃掉 1.2 GB。
测试设计:把变量压到最少
网上关于"某某语言写桌面应用"的文章,常见的问题是拿一个语言写的 Demo,去对比另一个语言"理论上应该很慢"的刻板印象。这种对比没什么信息量。
所以这次我想把变量压到最少:同一个应用、同一份前端、同一个后端接口、同一台机器、同一套测量脚本,只有后端实现语言不同。
具体是这样保证的:
- 前端 ui/index.html(3657 行)直接从 cj-tauri 的 examples/music 复制过去,逐行不改。唯一的例外是我在 Rust 侧加了 28 行桥接代码——因为 cj-tauri 的页面调的是 window.__CJ_TAURI__.invoke,而 Tauri 2 的全局对象叫 window.__TAURI__,名字对不上。这 28 行只是在两套 API 之间做适配,不碰任何业务逻辑。
- Rust 侧只重写后端:12 条 invoke 命令、HTTP 请求、封面代理、LRC 解析、收藏落盘,别的都不动。
- 两边连的后台是同一个本机 Go 服务(127.0.0.1:8000),music:hot 返回的是真实榜单(100 首,标题"网易热歌榜"),不是 mock 数据。
- 页面里本来就是一套 10 项自检(榜单、可播地址、封面代理、音频解码、歌词解析、高亮跟随、收藏读写、最近播放、歌单列表),两边跑的是同一套,所以"功能是否真的能用"这件事有客观对照。
两张图,同一套界面
先看一眼。这是 Rust + Tauri 2 跑起来的:

这是仓颉 cj-tauri 跑起来的:

同一个界面、同一个后台、同一份前端代码。两张图里榜单内容不一样,是因为榜单本来就在轮换;左侧"后台歌单"那几张封面也一致,说明封面代理这条链路两边都通。
顺便说明一个容易被误会的细节:图里左上角写着"cj-tauri WebView2"——这是前端里写死的文案,我照原样保留了(毕竟说好了前端一个字不改),在 Rust 版里它照样显示这几个字。所以别看这个标签判断我在跑哪个版本,看榜单内容或者看日志更靠谱。
编译效率:21 秒 vs 11 分 09 秒
这一项差距最大,也最容易被忽略。
仓颉侧(cjpm build,清空构建目录后冷编译,连框架自己那两万行一起编,6 轮):
- 墙钟时间 20.78 / 20.94 / 20.97 / 21.02 / 21.07 / 21.48 秒
- user 22.65–22.98 秒 / sys 3.88–3.99 秒
- CPU 占用 124–128%(这机器是 2 核)
- 构建进程峰值内存 174 MB
Rust 侧(cargo build –release,全新空 target 目录):
- 墙钟时间 11 分 09.85 秒
- user 654.50 秒 / sys 45.81 秒
- CPU 占用 104%
- 构建进程峰值内存 1.04 GB
这里面有几个数字值得展开说。
第一,Rust 那 104% 的 CPU 占用意味着它大部分时间是单核串行在跑(2 核机器上占满两核应该是 200%)。Tauri 的依赖链有相当一部分是串行等待的,所以加机器核心数不一定能救这个时间。仓颉侧的 124–128% 说明它稍微能并行一点,但也没吃满两核——两边都不是"靠核心多就能提速"的活。
第二,Rust 那边 Cargo.toml 锁到 444 个包,实际编译出 271 个 rlib。这就是 11 分钟的来源:不是你的代码慢,是依赖多。
第三,仓颉这边编译的东西少得多。框架自身也就 .cj 源码两万行加八千行 C 桥,示例应用一千多行,编译一遍对 2 核机器来说是二十秒的事。我还核对了一下,这 21 秒不是只编了示例那一层:target/release/.build-logs/ 底下同时生成了 music_app 和 cjTauri(含各 plugin 包)的编译日志,target 里落了 18 个 .cjo、17 个 .a,框架本体确实一起编了。
还要提醒一句:11 分钟是从零的数字,不是每天开工的数字。所以我又专门测了增量——改一个后端源文件再编(touch 一下,直接 cargo build –release / cjpm build,不清构建目录):
- Rust(touch src/main.rs):61.58 秒(首轮,含陈旧状态)、50.33 秒、50.16 秒;user 84.2–84.8 秒;构建峰值内存 806–867 MB。
- 仓颉(touch src/commands.cj):13.45 秒(首轮)、11.82 秒、11.86 秒、11.71 秒;user 18.7–20.9 秒;构建峰值内存 177–179 MB。
两边都有个反直觉的地方:仓颉的增量只比冷构建快 9 秒(11.8s 对 20.8s)。原因是 cjpm.toml 里 cjTauri = { path = "../.." } 是路径依赖,改一个后端包,框架那一层也要跟着重编。Rust 侧省得多(50s 对 669s),但那 50 秒照样不是"秒级"——tauri 的宏展开和泛型实例化全压在叶子 crate 上,光链接就要几十秒。
换成倍数看更直观:冷构建 32 倍,增量重编 4.3 倍。所以别用冷构建的数字判断日常体感,也别用增量的数字回避冷构建的代价。改一行等 50 秒和等 12 秒,是两种开发节奏,但 12 秒也不是"instant"。
还得补一句免得把结论用过头:这 32 倍主要来自要编的东西的体量差,不是编译器本身快慢——Rust 那边要编 444 个包里的 271 个 crate,仓颉这边是框架两万行加应用一千多行。所以"仓颉编译只要 21 秒"这个结论,在我测过的这个规模上成立;如果哪天出现一个几万行的仓颉应用,它会不会还是 21 秒,我没有样本可测,不敢替它保证。
最后交代一段踩坑经过,因为它正好能解释"为什么别人复现不出同样的数字"。这篇里原来的数字是 14.74 秒,那是在工具链没配对的环境里留下的。复核时我遇到的是 cjpm –version 直接 fatal(Check failed: itfVExtensionDataStart != nullptr),一度以为示例工程的 stdx 声明有问题,换着法子试了四种编译器与 stdx 路径的组合都编不过。
真正的根因很简单,也很蠢:这台机器装了两套仓颉 SDK,cjpm 只在其中一套里。1.2.0 的 cjpm 在 <安装>/tools/bin/cjpm,而我一直只把 <安装>/bin 挂进了 PATH,于是 cjc 是 1.2.0、cjpm 却是另一套的 1.0.5——两个版本差了整整一代,包管理器读不懂新编译器的元数据,才报出那个跟源码毫不相干的错。用官方脚本起环境(source /opt/cangjie-1.2.0/cangjie/envsetup.sh,它会把 bin 和 tools/bin 一起挂上)之后,cjpm build 一次就过了,既不用改 cjpm.toml,也不用动 stdx 声明。
所以这一节的数字是重测的:21 秒,6 轮极差 0.7 秒。我把它当成结论,14.74 秒那个撤掉——一个自己复现不出来的数字,写进文章只会给别人添麻烦。如果你也遇到"编译器和包管理器对不上"这类怪报错,先看 which cjc; which cjpm 这两个路径是不是同一次安装的,比怀疑代码有用得多。
磁盘:仓颉完胜,中间产物差 55–75 倍
这一项是我原本没打算测的,测完觉得比二进制体积更值得说,因为它直接关系到"你的开发机还剩多少空间"。
补一个实测到的事实:Rust 这边的数字不是固定的。冷构建结束时 target/ 是 871 MB;写这篇文章期间我又反复重建了几轮,它已经长到 1.2 GB(其中 target/release/deps 占 1.1 GB),对应 Cargo.lock 里锁的 444 个包、实际编出的 271 个 rlib。仓颉侧的 target/ 始终是 12–16 MB 量级,没有这种增长。
- Rust:纯 release 冷构建的 target 目录 871 MB;反复重建几轮后长到 1.2 GB。需要说清成因:这台机器上 target 里只有 release/、没有 debug/,1.2 GB 不是 debug 混进来的,是同一份 release 产物被反复重建、旧文件留在 deps 里堆出来的。
- 仓颉:target 目录 12 MB(动态链接 stdx 时);改成静态链接 stdx 是 16 MB,多出来的 4 MB 是框架自身的 .cjo/.a 产物。
同一个应用、同样的功能,相差七十多倍。
环境本身也占盘:SDK 和工具链
中间产物之外,装环境这笔账我也拆开量了:
| 编译器/工具链主体 | 1.8 GB(~/.rustup,其中 share/doc 801 MB 是 rust-docs 文档) | 633 MB(/opt/cangjie-1.2.0/cangjie:lib 159 MB、third_party 165 MB、tools 116 MB、bin 70 MB、modules 64 MB、runtime 58 MB) |
| 包管理缓存与依赖 | 400 MB(~/.cargo:依赖源码 184 MB、包缓存 125 MB、索引 50 MB、bin 43 MB) | 186 MB(stdx 1.2.0.1 单版本) |
| 合计 | 2.2 GB | 0.82 GB |
但这里必须把话说准,否则很容易吹过头:
- 默认安装形态是 2.2 GB 对 0.82 GB,2.4 倍。不过 Rust 那 801 MB 是文档,rustup component remove rust-docs 一条命令就能删;删完 toolchain 剩约 1.0 GB,只比编译器和 SDK 本体就缩到 1.2 倍;如果连 ~/.cargo 的包缓存一起算但仍剔除文档,是 1.4 GB 对 0.82 GB,1.7 倍。
- ~/.cargo 那 400 MB 里有 309 MB 是 444 个依赖的源码与包缓存,属于"这个项目带来的",还能跟别的 Rust 项目共享,不该全算在语言环境头上。
- 所以准确说法是:环境体量仓颉赢,但只是 1.2–2.4 倍,不是数量级。数量级差距在中间产物(55–75 倍)和编译时间(冷构建 32 倍、增量 4.3 倍)上。
- 还得交代一句本机情况:这台机器同时留着两套仓颉 SDK(1.0.5 与 1.2.0)和两版 stdx(1.2.0.1 186 MB + 1.0.5.1 43 MB),上面按单套 1.2.0 算。多版本共存两边都有——rustup 装两个 toolchain 就是 3.6 GB。
顺带说明:~/.cargo/registry 的 358 MB 跟历史项目共享,所以我没把它算进项目对比,只在上面这张表里单列。
这台机器的根分区一共 50G,长年只剩十几 G 可用。对这个体量的机器来说,871 MB 的中间产物不是抽象数字——多开两个 Tauri 项目,几个 G 就没了。如果你也是小盘机器,这一点值得在选型时算进去。
产物体积:单文件是仓颉小,装完是 Rust 小
先说最容易误导人的那组数字。
只看单文件:
- Rust 主二进制:13,711,080 字节 = 13.08 MB;strip 之后 10,131,784 字节 = 9.66 MB
- 仓颉主二进制:7,715,192 字节 = 7.36 MB;strip 之后 6,390,832 字节 = 6.09 MB
单看这一层,仓颉小一半,很多人到这里就结束了。但我用 ldd 把依赖摊开看了一下,情况就变了。
仓颉这边有个容易漏掉的开关:CANGJIE_STDX 指向 static/stdx 还是 dynamic/stdx,决定 std 和 stdx 是静态链进主二进制,还是作为外部动态库存在。同一份源码,两种配置我都编了一遍,产物形态差别很大:
配置一:动态链接 stdx(cj-tauri 文档里的口径,CANGJIE_STDX 指 dynamic/stdx)
- 主二进制 7,715,192 字节 = 7.36 MB,strip 后 6,390,832 字节 = 6.09 MB
- ldd 里有 45 个仓颉系库要随包带:libcangjie-runtime、二十多个 libcangjie-std-*、十几个 libstdx.*、libcjtbridge、libboundscheck
- 这批库我全部 strip 了一遍,合计 15.19 MB(strip 只省了 0.02 MB,基本没得压)
- 分发净体积:6.09 + 15.19 = 21.28 MB
配置二:静态链接 stdx(CANGJIE_STDX 指 static/stdx)
- 主二进制 12,114,312 字节 = 11.55 MB,strip 后 9,920,320 字节 = 9.46 MB
- 只剩 4 个仓颉系库要带:libcangjie-runtime、libpcre2-8、libboundscheck、libcjtbridge,strip 后合计 4.76 MB
- 分发净体积:9.46 + 4.76 = 14.22 MB
也就是说,静态链接能省下 10 MB 分发体积,代价是主二进制胖 3.37 MB。体积没有消失,只是从"一堆 .so"搬进了"一个 ELF"。想给用户一个目录拷走就能跑的包,配置二更实在;想宣传单文件小,配置一好看。
Rust 这边除了系统库,没有需要随包带的运行时——WebKitGTK 是系统依赖,两边一样要。
所以真正要发给用户的体积是:
- Rust:9.66 MB(就一个可执行文件)
- 仓颉:21.28 MB(动态配置)/ 14.22 MB(静态配置)
两边共有的系统库(WebKitGTK 一个就 90 MB,加上 ICU、GStreamer 等)合计 206 MB,这部分不计入差异,因为无论用哪个框架都得有。
结论是:仓颉的单文件更小,但整体分发体积更大,多出来的正是语言运行时;即便切到静态链接把差距缩到 4.56 MB,Rust 的单文件交付依然是最省心的。这一点在文章里很少被提到,但做发布打包的人一定会遇到。
启动与运行:打平,但过程不太一样
这一项我写得比较小心,因为测法不对很容易得出相反结论。测法是:自己启动进程、逐行读 stderr 打时间戳,两边跑同一份脚本(measure.py);内存另用 bench/mem_tree.py 读 /proc/<pid>/smaps_rollup 的 PSS/USS;两边都在 xvfb-run + dbus-run-session 里跑,环境变量也保持一致。
先说环境为什么重要。第一次测的时候,Rust 版从进程启动到窗口创建花了 30.5 到 31.5 秒,连测三次几乎一模一样,而仓颉版只要 0.57 秒。我一开始真的以为是 Tauri 初始化慢,还去查了 WebKit 的 DMA-BUF 渲染器、无障碍总线,都排除了。
最后用 strace 抓 connect 的时序才看明白:进程向 D-Bus 会话总线 sendto 了一句 BEGIN 认证握手,然后干等 25 秒才重连。这是容器里会话总线的激活超时,跟 Tauri 一行代码都没关系。改用 dbus-run-session 起一个独立总线之后,Rust 版窗口创建时间直接从 31 秒掉到 0.34 到 0.56 秒。
这件事值得单独强调,因为它是个方法论问题:在同一个环境里测两个程序,环境本身可能只惩罚其中一个。如果我偷懒,直接拿最初那组数据写文章,就会得出"Tauri 启动比 cj-tauri 慢 50 倍"这个完全错误的结论。所以后文所有数字,都是两边统一加了 dbus-run-session 之后重新测的。
统一口径下的时间线(同一台机器,同一份前端,同一套脚本):
Rust + Tauri 2:
- 进程首行日志:+0.115 秒
- 窗口创建:+0.477 秒
- 桥就绪、首屏请求发出:+1.104 秒
- 首屏数据到达:+1.802 秒
- 自检开始:+2.520 秒
- 自检汇总:+5.049 秒
- 进程退出:+5.588 秒,退出码 0
仓颉 cj-tauri:
- 进程首行日志:+0.305 秒
- 窗口创建:+0.313 秒
- 桥就绪、首屏请求发出:+1.229 秒
- 首屏数据到达:+1.990 秒
- 自检开始:+2.867 秒
- 自检汇总:+5.092 秒
- 进程退出:+5.620 秒,退出码 0
逐项看,两边互有胜负,但差距都在零点几秒的量级,用户感知不到。真正对齐的是首屏数据到达这一步——1.80 秒对 1.99 秒,差 0.19 秒,而这 0.19 秒里还包含了一次真实的网络请求(本机后台 + 网易封面外链),本来就有抖动。
有一点我必须说明:两边的"窗口创建"里程碑语义不完全等价。仓颉那边的日志是 host created,打在宿主对象建好时;Tauri 那边是窗口加 webview 建好之后。所以我更信"首屏数据到达"这个点——它意味着页面真的跑起来并完成了第一次接口调用,两边语义一致,不会因为日志打点位置不同而失真。
内存:先纠正一个很容易测错的地方
先说结论:两边都在几百兆,同环境下差距只有 5%–15%;这一项的主导变量是 WebKit 的合成开关,不是语言。
我第一版测这组数用的是最省事的办法:读每个进程 /proc/<pid>/status 的 VmRSS,把宿主和 WebKit* 子进程的 RSS 加起来。这个数是错的——RSS 会把共享页在每个进程里各记一遍(每个进程都映射着同一批 .so、字体缓存、共享内存段),所以加出来是 430 MB(Rust)/ 664 MB(仓颉),比真实占用高出一大截。像「一个播放器怎么吃几百兆」这种印象,多半就是这个口径闹的:仓颉版宿主进程 RSS 190 MB,其中 107 MB 是共享页,它独占的只有 83 MB。
改读 /proc/<pid>/smaps_rollup,两个更靠谱的量:
- PSS(Proportional Set Size):共享页按共享它的进程数均分,整棵树相加不重复计,等于这套应用真实占了多少物理内存;
- USS(Private_Clean + Private_Dirty):进程独占、进程退出就能真正还回去的部分。
同口径测(两边都 xvfb-run + dbus-run-session、同一份 bench/mem_tree.py、同一个本地后端、都开自检、都关合成):
| 宿主进程 PSS / USS | 90.6 / 56.8 MB | 120.0 / 83.0 MB |
| WebKit 渲染进程 PSS / USS | 140.7 / 112.1 MB | 136.4 / 104.3 MB |
| WebKit 网络进程 PSS / USS | 32.3 / 20.2 MB | 33.4 / 21.0 MB |
| 全树 PSS 峰值 | 267 MB | 308 MB |
| 全树 USS 峰值 | 193 MB | 226 MB |
两边各跑 2 轮,全树 PSS 峰值分别是 267.2 / 267.6 MB 和 308.0 / 306.0 MB,抖动在 1 MB 量级。
关键在这两行对比:仓颉版总共多占约 41 MB(+15%),而且多出来的几乎全在它自己的宿主进程上(PSS 差 29 MB);WebKit 渲染进程那一侧不但没多,反而略少 4 MB。 也就是说,差的不是"仓颉的 webview 配置",是仓颉运行时本身在宿主进程里的常驻开销。
还有一个比语言影响大得多的变量:WebKit 的合成模式。同一个二进制,只把 WEBKIT_DISABLE_COMPOSITING_MODE=1 去掉,渲染进程的 PSS 就从 140.7 MB 涨到 335.6 MB(Rust)、136.4 MB 涨到 332.6 MB(仓颉),全树一起翻倍:
| 关合成(对等,各 2 轮) | 267.2 / 267.6 MB | 308.0 / 306.0 MB |
| 开合成(对等,各 1 轮) | 458.4 MB | 484.4 MB |
两边的差在同一环境里始终是 5%–15%,远小于合成开关带来的那近 200 MB。所以我这一项的判断是:仓颉版确实略高,但差额不够当决策依据;如果看到"差 200 MB"的结论,先去看它的口径。 另外这套机器没有 GPU、纯软件渲染,几十张封面解码后全驻留,绝对数值本来就比带 GPU 的桌面环境难看,请按自己的机器打折。
功能是不是真的能用:10 项自检,两边都是 10/0
体积和内存都是手段,能用才是目的。页面里自带 10 项自检,我原封不动地让两边都跑了一遍。
Rust 侧输出:
- PASS ① 热门榜单取到数据 — 30 首,榜首:求你别提分手 – 余佳运
- PASS ② 榜首带可播地址与 sid — url=https://music.163.com/song/media/outer/url?id=3397958994
- PASS ③ 封面经宿主代理取回 — 5615 字节,前缀 data:image/jpeg;base64…
- PASS ④ 音频真的在解码播放 — paused=false currentTime=0.51s duration=180.2s readyState=4
- PASS ⑤ 歌词解出行 — 32 行
- PASS ⑥ 歌词高亮跟随播放位置 — 跳到第 5 行后 lyricIdx=4
- PASS ⑦ 高亮行带 .active 类
- PASS ⑧ 收藏写入并能读回 — songs=1
- PASS ⑨ 最近播放记下了当前曲目
- PASS ⑩ 后台歌单列表取回 — 6 张
- 汇总:10 passed / 0 failed
仓颉侧输出(同样 10 项):
- PASS ① 热门榜单取到数据 — 30 首,榜首:求你别提分手 – 余佳运
- PASS ② 榜首带可播地址与 sid — sid=3397958994
- PASS ③ 封面经宿主代理取回 — 5615 字节
- PASS ④ 音频真的在解码播放 — currentTime=0.51s duration=180.2s readyState=4
- PASS ⑤ 歌词解出行 — 32 行,首行「房间仍有你的香味」
- PASS ⑥ 歌词高亮跟随播放位置 — 跳到 35220ms 后 lyricIdx=4
- PASS ⑦ 高亮行带 .active 类 — 「你笑得像傻蛋」
- PASS ⑧ 收藏写入并能读回 — songs=1
- PASS ⑨ 最近播放记下了当前曲目
- PASS ⑩ 后台歌单列表取回 — 6 张,首张「猫哥的收藏」
- 汇总:10 passed / 0 failed
第 ④ 项是我最看重的:它不是"接口返回 200",而是真的在解码播放——currentTime 从 0 走到 0.51 秒,readyState=4,时长 180.2 秒。两个版本都过了,说明 WebKit 的音频管线在两边都跑通了。第 ③ 项也值得看一眼:封面是宿主侧带上 Referer: https://image.baidu.com/ 去取的,不带这个头会返回 200 但 0 字节,两边都正确处理了这个坑。
代码量:897 行对 1349 行
这部分数据要打个折扣看,我先说清楚为什么。
Rust 侧(我从零写的后端):6 个 .rs 文件,共 897 行,其中代码 726 行、注释 99 行、空行 72 行。包括 12 条命令分发、HTTP、封面代理、LRC 解析、收藏落盘。
仓颉侧(仓库自带的示例后端):7 个 .cj 文件,共 1469 行,扣掉两个资源打包文件(120 行)之后是 1349 行。
单看行数,Rust 侧少了三分之一。但我必须诚实说明:这两份代码的"作者成本"不可比。仓颉那份是 cj-tauri 官方示例,本来就在仓库里,我这次只是把它复制到仓库外重新构建;Rust 那份是我对着 12 条命令契约一条条实现的。所以"897 行"里其实藏着我读仓颉代码、搞清楚契约行为的时间成本。
行数差异也有客观原因:库不一样。Rust 这边 serde_json、reqwest 帮着干掉了很多样板;仓颉那边 stdx 也在做同样的事,但示例作者在 LRC 解析和封面处理上写得更"手写",比如时间戳要自己归一化、[by:…] 行要自己丢弃、一行多个时间戳要自己拆。这些逻辑两边都得有,只是放在哪一层不同。
所以这一项我的结论是:行数不能证明哪边写起来更快,只能说明同等功能在两边需要的代码量在同一个数量级。真要比开发效率,得让同一个人用两种语言各写一遍新需求,我这次做不到那个条件。
踩到的坑,两边都有
写这种对比最有价值的部分其实是坑,因为数字可以复现,坑只有踩过的人知道。
Rust/Tauri 侧:
- 前面说的 D-Bus 25 秒等待。这个坑最阴险——不查的话会得出完全错误的性能结论。
- tauri.conf.json 里 frontendDist 必须写 "ui",写成 "../ui" 会在构建阶段报错。这个错误信息不太友好,我绕了一圈才定位到。
- icons/icon.png 缺失会让 generate_context! 这个宏直接 panic,错误是 failed to open icon。也就是说图标不是"可选美化项",是构建硬依赖。
- 12 条命令名要改。cj-tauri 那边叫 music:hot(带冒号),Tauri 2 里同名命令不允许这种写法,我统一换成下划线 music_hot,对应的 28 行前端适配也就是改这个。
仓颉/cj-tauri 侧:
- 我第一次把 ui/ 和二进制复制到一个干净目录里跑,结果页面在疯狂重试、日志刷屏。原因是那个目录里没有 capabilities/,而 cj-tauri 的权限模型是默认全拒绝:没有能力声明文件,所有命令一律不让调。这个设计我认为是对的(安全默认值),但报错表现是"前端一直重试",需要看宿主启动日志才发现那几行"尚未授权"的提示。如果日志能更醒目,或者页面上直接显示"命令被拒绝",会省很多时间。
- 在上面那次缺 capabilities 的运行里,进程退出时出现过 SIGABRT(退出码 134),栈是 catched unhandled SIGABRT from runtime frame。补齐能力声明、并且用 dbus-run-session 跑之后,退出码恢复正常 0。所以这个崩溃我没能稳定复现,就不算它账上了——但记一笔在这里。
- cjpm 与 cjc 不在同一个目录:编译器在 <安装>/bin/cjc,包管理器在 <安装>/tools/bin/cjpm,两个都得进 PATH,否则报的是"failed to run ‘cjc -v’"这种看起来跟代码无关的错。
- 两套 SDK 混用是本篇踩得最久的一个坑。机器上同时装了 1.0.5 和 1.2.0,我只把 1.2.0 的 bin/ 挂进了 PATH,于是 cjc 是 1.2.0、cjpm 落到 1.0.5 那份。表现是 cjpm –version 直接 Check failed: itfVExtensionDataStart is nullptr 崩掉,cjpm build 则报出"某个 stdx 包没写进 [dependencies]"——这条报错是假的,改 cjpm.toml 怎么改都没用。正确做法是用官方 envsetup.sh 起环境,它会把 bin 和 tools/bin 一起挂上;配对之后同一份 cjpm.toml 一个字没改就编过了。判断方法很土但有效:which cjc; which cjpm 看两个路径是不是同一次安装的。
- stdx 的目录布局两个版本不一样:1.0.5.1 的 .cjo 在 <stdx>/static/stdx/,1.2.0.1 在 <stdx>/linux_x86_64_cjnative/static/stdx/。CANGJIE_STDX 指错一层,cjpm 就会报"can not find path … listed in bin-dependencies",跟源码一点关系都没有。
所以到底用哪个
我不打算给出"选 A"这种结论,因为这两套东西的差别不在性能,而在你要付出什么代价。
如果你的项目符合这些情况,用 Rust + Tauri 更稳:
- 需要成熟的第三方库。Rust 这边要 JSON、要 HTTP、要 TLS,serde_json + reqwest 一行依赖搞定;仓颉这边得靠 stdx,能用,但可选范围不是一个量级。
- 交付形态是一个可执行文件、越小越好。Rust 9.66 MB 装完就这些东西,不用管运行时库往哪放。
- 团队已经会 Rust,或者你在意招人、社区问答、踩坑资料的存量。这一项 Rust 的优势很大,遇到问题基本能搜到答案。
- 机器内存吃紧,或者只能软件渲染。这一项 Rust 略省(同环境 267 MB 对 308 MB),但差额只有 41 MB——够不成决策理由,别拿它当主要依据。
如果你的项目符合这些情况,cj-tauri 的吸引力就出来了:
- 你在用仓颉,需要给内部工具套一个桌面壳。这是它最舒服的定位:不用为了界面去学一整套 Rust 生态。
- 你在意编译等待时间。21 秒对 11 分钟,这个差距会实打实改变你的开发节奏——尤其是改一行界面文案都要重编的时候。
- 磁盘紧张。12 MB 对 871 MB 的中间产物,小盘机器上差别很明显。
- 你接受"运行时要一起发"这件事。21.28 MB 的净分发体积不算大,但得知道它比单文件看起来的大小多了 15 MB;想缩到 14.22 MB,就把 CANGJIE_STDX 指到 static/stdx 走静态链接。
还有不少东西我没测,不能装作测过——我把它们单独列成下一节,包括长时运行、真实出声、交叉编译这些。选型前请先看一眼那一节,知道自己拿到的结论覆盖了哪些、没覆盖哪些。
还没测到的部分
一节诚实清单。这些缺口不影响前面「编译、磁盘、启动、内存」的结论——那几项我每个都跑了多轮;但它们会影响「能不能上生产」的判断,所以摆明比含糊过去更有用。
- 跨平台。全部在 Linux + Xvfb 下测。cj-tauri 在 Linux 走 WebKitGTK,Tauri 在 Windows 走 WebView2、macOS 走 WKWebView,是三条不同的渲染路径,性能结论不能跨平台照搬。仓颉的 Windows 目标(x86_64-w64-mingw32)我连编译都没跑过。
- 长时运行与内存泄漏。自检只跑 5 秒,播放一小时内存曲线是否持续上涨、反复开关窗口是否有泄漏,没测。这一项对桌面应用比对编译速度重要得多。
- CPU 占用。播放中和空闲时的 CPU 百分比没采。这台机器只有 2 核而且跑着 Xvfb,采出来容易失真,我不想给个不准的数。
- 真实出声链路。自检只验到「解码在跑」——readyState=4、currentTime 从 0 走到 0.51 秒。容器里没有音频设备,耳机里是否真出声、从点击到出声的延迟多少,测不了。
- 交互性能。列表滚动 FPS、点击响应延迟、1000 首大列表的渲染耗时,都没测(自检固定 30 首)。这恰恰是用户最能感知的一层。
- 网络与并发。两边 HTTP 的并发能力、TLS 握手开销没测。
- 依赖拉取时间。cargo 首次拉 444 个包、仓颉拉 stdx 的耗时,我只记了「很久/还行」,没有系统计时。
- 打包发布链路。安装包、自动更新、代码签名、产物压缩,这些是工程活,两边都得另干,我没碰。
- 异常降级。后端 500、断网、封面 404 时两边的表现(尤其错误提示是否可读),只走了正常路径,没测。
- 可调试性。只有踩坑记录里的主观感受,没有量化——比如同一类错误两边报错信息的信息量对比。
- 二进制加固属性。PIE、RELRO、stack canary 这些两边产物到底开了没有,没查。
如果后面继续做,我会优先补长时内存曲线和交互性能这两项:一个决定稳定性,一个决定"用起来爽不爽"。
怎么复现
想自己验的话,两边各一条命令。前提是本机有个返回榜单的后台接口,环境变量 CJ_MUSIC_API 指过去就行。
Rust 侧:
cd /root/test/rust-music/run
python3 ../bench/measure.py –label rust –cwd . –seconds 60 \\
CJ_MUSIC_API=http://127.0.0.1:8000 CJ_MUSIC_SELFCHECK=1 \\
CJ_MUSIC_LIBRARY=./library-rust.json WEBKIT_DISABLE_COMPOSITING_MODE=1 \\
— xvfb-run -a dbus-run-session — ../target/release/rust-music
仓颉侧:
cd /root/test/cjg-build/examples/music
python3 /root/test/rust-music/bench/measure.py –label cj –cwd . –seconds 60 \\
CJ_MUSIC_API=http://127.0.0.1:8000 CJ_MUSIC_SELFCHECK=1 \\
CJ_MUSIC_LIBRARY=/root/test/cj-music-run/library-cj.json \\
— xvfb-run -a dbus-run-session — ./target/release/bin/main
脚本会逐行给日志打时间戳,跑完直接打印时间线;内存另外用 bench/mem_tree.py 采 PSS/USS(别用各进程 RSS 相加,那个数会重复计共享页)。编译耗时用 time cargo build –release 和 time cjpm build 就行,记得先清空构建目录,否则测的是增量。
总结
一句话概括:这不是"新语言打败老语言",而是一道"用磁盘和等待时间换生态成熟度"的选择题。
把你的约束往左边一列去找,右边就是我的建议和依据:
| 要啥库都得有,指望 AI 直接照抄现成答案 | Rust + Tauri | Cargo.lock 里 444 个包;JSON/HTTP/TLS 一行依赖解决 |
| 交付就一个文件,越小越好 | Rust + Tauri | 9.66 MB 单文件,无外部运行时 |
| 磁盘紧、手上项目多 | cj-tauri | 中间产物 12–16 MB 对 871 MB–1.2 GB(55–75 倍);环境也是小的,0.82 GB 对 1.0–2.2 GB |
| 痛苦点在等编译 | cj-tauri | 冷构建 21 秒对 11 分 09 秒;增量 12 秒对 50 秒 |
| 运行速度要当卖点 | 谁都行 | 建窗 0.31 秒对 0.48 秒、首屏 1.80 对 1.99 秒,差距在噪声范围内 |
| 内存要当卖点 | 谁都别用 | 同环境 267 对 308 MB,差 41 MB,不到合成开关影响的四分之一 |
| 团队在写仓颉,要给内部工具套壳 | cj-tauri | 它最舒服的定位,不用为界面去学 Rust 生态 |
再补一句 AI 时代的具体建议。AI 把"写"变便宜以后,语言选择的权重其实变了两个地方:生态决定 AI 能不能直接照抄成熟答案(Rust 这边概率高得多),磁盘和编译决定你验证 AI 产出的周转速度(仓颉这边明显快)。所以我自己现在的分法是:原型、内部工具、本机小盘环境用 cj-tauri 试;要发给外部用户、依赖大量第三方库、需要单文件分发的正式项目,仍然用 Rust。
写这篇的动力其实很朴素:我在用 cj-tauri 做东西,但一直想知道它和成熟方案的真实差距在哪、差多少。
网上找不到这类同口径的实测,那就自己做一遍。
做完之后我的判断是:cj-tauri 在编译速度和磁盘占用这两项上赢得干净利落,在生态、分发体积这两项上落后,内存略高(+15%,而且要同环境才可比,不构成差距),运行速度和功能完整度基本打平。它不是"更快的 Tauri",它是一个在特定取舍下更合适的选择——你要的是开发迭代快、机器负担小,它能给你;你要的是生态和单文件交付,它暂时还给不了。
这些数字都是本机实测,环境写在上面了,脚本也贴了。如果你在别的机器上跑出不一样的结论,那很可能是我这台 2 核小盘机器放大了某些差异——欢迎拿数据来对。





