大家好,我是何三,独立开发者

mquickjs

曾在 Hacker News 首页被一个项目刷屏了——1500+ upvote,569 条评论,讨论热度快赶上 AI 新框架发布了。

主角不是什么大厂的重量级产品,是一个用纯 C 写的 JavaScript 引擎,核心代码文件不到 20 个,总共才 45 个 commit。

10kB RAM 就能跑 JS 代码。整个引擎加 C 库才 100kB ROM。

Chrome 用的 V8 引擎要吃掉大约 100MB 内存。Bellard 自己之前写的 QuickJS,算是已经很轻量了,也要 ~200KB。

MQuickJS 比 V8 小了一万倍。

一万倍。我打了这几个字又删了三遍,确认没写错。

mquickjs

项目叫 MicroQuickJS,作者 Fabrice Bellard。

这名字听着陌生?没关系。FFmpeg 你肯定知道——全球最流行的多媒体处理框架,几乎所有视频网站底层都在用它。QEMU 也是他写的——云服务商跑虚拟机的标配。还有 QuickJS 本身,已经够轻量了,他嫌还不够小,于是又撸了一个 MQuickJS。

法国人,一个人写。程序员圈里管他叫"上帝之手"。

我有时候在想,这老哥是不是有什么特殊的降维打击能力——别人做一个项目要团队几十人迭代好几年,他一个人在巴黎的公寓里,隔两年就扔出来一个重新定义行业标准的东西。这已经不是"厉害"能形容的了,怎么说呢,就是那种——你玩游戏碰到开了全图挂的大佬,你只能观战。

怎么把 200KB 塞进 10KB 里

Bellard 的思路说白了就一句话:能不用的全砍,能换的全换,能用 1 bit 解决的绝不用 2 bit。

内存分配:C 标准库的 malloc 和 free?不要了。MQuickJS 自带一套内存分配器,你只需要给它一块 buffer:

uint8_t mem_buf[8192];  // 8KB,够了
JSContext *ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib);

引擎只在你给的这块 buffer 里活动,绝不越界。buffer 满了,GC 自动回收。这设计其实有点像给引擎租了个小公寓——你爱怎么折腾都行,但别出门。

垃圾回收:QuickJS 用的是引用计数(reference counting),问题是用久了内存碎片一堆,跟硬盘用久了要碎片整理一个道理。MQuickJS 换成了追踪式压缩 GC(tracing + compacting),对象可以搬家,内存整整齐齐挤在一起,一点缝都不留。

这个压缩 GC 的实现细节说实话我也没完全搞懂,源码看了一半就放弃了——有兴趣的可以去翻 gc.c,有懂的大佬欢迎指正。

标准库:QuickJS 运行时要创建一堆内置对象(Object、Array、Function 之类的),每一个都要占 RAM。MQuickJS 把整个标准库在编译时就转成 C 结构体写进了 ROM。运行时?运行时几乎不用额外 RAM。等于把家具全焊死在墙上了,省了搬家的空间。

字符串编码:JavaScript 标准规定字符串底层是 UTF-16,每个字符最少占 2 字节。但现实是大部分 JS 代码写的都是英文和 ASCII,每个字符其实 1 字节就够了。MQuickJS 用了 WTF-8 编码(对,就叫这个名),英文 1 字节,中文 3 字节,动态自适应。光这一改,字符串的内存占用就砍了一大半。

解析器:QuickJS 的解析器是递归的,遇到深度嵌套的代码可能会把 C 栈撑爆。在内存只有几十 kB 的环境里,这可是致命的。MQuickJS 的解析器刻意避免了递归,栈深度有严格上界。

mquickjs

每一个改动单看都不算什么,但全堆在一起——效果就是把 200KB 的引擎塞进了 10KB 的 RAM 里

这种工程能力,怎么说呢,服了。

代价是什么

天下没有免费的压缩。MQuickJS 只支持 ES5 的一个子集,还强制开了严格模式。一些 JS 的"坏习惯"直接不让用:

  • 数组不能有空洞。a[0] = 1; a[10] = 2; 直接报错
  • 不支持 constlet,只能用 var
  • Date 对象只有 Date.now(),完整的日期操作?没有
  • 不支持 new Number(1) 这类值装箱
  • 正则只有 ASCII 的大小写折叠

HN 上有人吐槽:"Date 只支持 Date.now(),这在实际项目里会炸的。"

也会有人问"能不能跑 TypeScript?"

不能。也不应该。

MQuickJS 不是给你跑 npm 包的。它的战场是 STM32、ESP32 这些 RAM 只有几十 KB 的微控制器。在这种设备上,你写的是控制传感器、读写 GPIO、处理简单协议的嵌入式脚本。你指望在上面跑 React?想什么呢。

不过 Simonw(Datasette 的作者)想了一个很妙的场景:在服务端跑用户提交的自定义 JS 脚本。用户写的代码可能很蠢甚至恶意,但 MQuickJS 自带的内存限制就是天然沙箱——你给了它 10KB,它就算想作恶也翻不出这个 buffer。他甚至让 Claude Code 花了 15 个小时把 MQuickJS 移植到了纯 Python,402 个测试只失败了 2 个。

跑一下试试

官方给了一个很能打的 demo:用 10kB 内存限制跑 Mandelbrot 分形图。

# 克隆仓库
git clone https://github.com/bellard/mquickjs.git
cd mquickjs
make

# 用 10kB 内存限制跑 Mandelbrot 测试
./mqjs --memory-limit 10k tests/mandelbrot.js

就这一条命令。Mandelbrot 分形在你终端里被计算出来,而整个 JS 引擎只吃了 10kB 内存。

还有字节码模式——把 JS 编译成字节码,直接烧进芯片 ROM 里运行:

# 编译为字节码
./mqjs -o mandelbrot.bin tests/mandelbrot.js

# 运行预编译字节码
./mqjs -b mandelbrot.bin

连编译阶段的开销都省了。这玩意儿真的是为嵌入式而生的。

会改变什么?

坦白说,MQuickJS 不会影响你写 React、用 Vite、在 Chrome 里调试代码的日常。该干嘛干嘛。

但它打开了一扇门——JavaScript 不再只是浏览器的语言,也不只是 Node.js 的语言,它可以跑在一颗 2 美元的微控制器上。

以前想在嵌入式设备上跑脚本,你的选择基本是 Lua 或 MicroPython。Lua 是不错,但生态毕竟小。MicroPython 嘛……性能堪忧。JavaScript 作为全球使用人数最多的编程语言,终于有一个能塞进芯片的引擎了。这对 IoT 开发者、硬件工程师、创客社区来说,真的是一个挺大的事。

HN 上已经有人把它移植到了 ESP32 上,用的还是 Claude Code 辅助移植——这条路跑通了。

一句话总结:MQuickJS 证明了 JavaScript 可以小到塞进 10kB 的内存里。有各种限制不假,但把一门现代编程语言压缩到极致、跑在最卑微的硬件上——这种工程美学,真不是每天都能碰到的。

本文使用 MGO 编辑并发布

关注"何三笔记",回复"mgo" 免费下载使用