Micro QuickJS:Fabrice Bellard 把 JS 引擎压进 10 KB 内存的嵌入式实验
阅读时间: 大约 10 分钟
Micro QuickJS:Fabrice Bellard 把 JS 引擎压进 10 KB 内存的嵌入式实验
Micro QuickJS(MQuickJS)是 Fabrice Bellard 与 Charlie Gordon 发布的 JavaScript 引擎,目标场景非常明确——嵌入式系统。官方 README 给出的硬指标是:用低至 10 kB 的 RAM 即可运行 JavaScript 程序,整个引擎(含 C 库)约需 100 kB 的 ROM(ARM Thumb-2 代码),运行速度与 QuickJS 相当。它不是”又一个 Node 替代品”,而是一次把 JS 引擎做到能塞进资源极度受限芯片里的工程实验。本文依据其官方 README 做一手拆解。

一、它解决的是什么问题
普通 JavaScript 引擎(V8、JavaScriptCore、甚至 QuickJS)即便”轻量”,也是按几十 MB 量级的主机内存设计的。而在微控制器、IoT 传感器、可穿戴设备这类场景里,可用 RAM 常常只有几 KB,ROM 也以几十 KB 计,并且往往没有文件系统、没有 malloc、甚至没有浮点数单元。MQuickJS 就是为这种硬件预算而生:它把引擎本身做小、把内存分配器内置、把标准库直接烧进 ROM。
二、核心数字(官方 README 口径)
| 维度 | MQuickJS 官方称 |
|---|---|
| 最低运行 RAM | 约 10 kB(--memory-limit 10k 跑 mandelbrot.js) |
| 引擎 + C 库 ROM 占用 | 约 100 kB(ARM Thumb-2) |
| 速度 | 与 QuickJS 相当 |
| JS 语言支持 | 接近 ES5 的子集,恒为”stricter mode” |
| 垃圾回收 | 追踪式 + 压缩式(compact) |
| 是否用 CPU 栈 | 否(VM 不使用 C 栈) |
| 字符串内部编码 | WTF-8(UTF-8 + 孤立代理对) |
| Value 尺寸 | 一个 CPU 字(32 位 CPU 上即 32 位) |
| 最小 JS 对象 | 3 个 CPU 字(32 位 CPU 上即 12 字节) |
注意”10 kB RAM”是跑通 mandelbrot.js 这类微型脚本的下限,不是跑任意真实应用的数字;ROM 的 100 kB 也含标准库,具体随裁剪策略浮动。

三、技术机制:为什么能这么小
1. 自带分配器,彻底不依赖 libc
MQuickJS 几乎不依赖 C 库:它不使用 malloc()、free(),连 printf() 都不用。创建上下文时由调用方提供一块内存缓冲:
uint8_t mem_buf[8192];
JSContext *ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib);引擎只在这块缓冲里分配内存,JS_FreeContext() 仅用于触发用户对象的 finalizer——因为系统内存根本没被申请。这对无操作系统的裸金属环境是决定性的。
2. 压缩式 GC 取代引用计数
QuickJS 原本用引用计数做 GC,对象要带引用计数开销。MQuickJS 换成追踪 + 压缩式 GC:每个分配块只多花几个比特的开销,同时消除内存碎片。代价是——因为对象会在每次分配时移动,C 代码里不能长期持有 JSValue 值,必须通过 JS_PushGCRef()/JS_PopGCRef() 把临时引用放进由引擎维护的槽位里。这是它 C API 与普通引擎最大的不同。
3. Value 压到一个 CPU 字
在 32 位 CPU 上,一个 Value 只有 32 位,可表达:1 个带 1 位 tag 的 31 位整数、单个 Unicode 码点(含一两个 16 位 code unit 的字符串)、小指数的 64 位浮点,或一个指向内存块的指针。JS 对象最小只要 3 个字,属性存在哈希表里、每条属性至少 3 个字。标准库对象的属性甚至可以常驻 ROM。
4. 字节码直接烧进 ROM
mqjs -o mandelbrot.bin tests/mandelbrot.js 可把编译后的字节码导出;烧入 ROM 前用 JS_RelocateBytecode() 重定位,之后 JS_LoadBytecode() + JS_Run() 直接执行。标准库本身在编译期被生成成 C 结构、驻留 ROM,因此引擎实例化几乎不花 RAM。调试信息里的行号/列号用指数-Golomb 编码压缩以省存储。
四、“stricter mode”:它主动砍了哪些语法
为了少出错、少占内存,MQuickJS 恒处于 stricter mode,主动禁用一批易错或低效的构造:
- 没有
with语句;全局变量必须用var声明; - 数组不能有洞:
a[10]=2而数组只到a[0]会抛TypeError,带洞的字面量[1,,3]直接语法错误;需要稀疏结构就用普通对象; - 不做装箱:
new Number(1)不被支持(官方称也从不必要); - 只支持全局间接
eval,不能访问/修改局部变量; - 正则的大小写折叠只处理 ASCII,匹配按 Unicode 码点;
toLowerCase/toUpperCase只处理 ASCII; - Date 只支持
Date.now()。
官方称这些限制是”一个 JS 子集”:写成 MQuickJS 能跑的代码,搬到别的引擎上照常工作——它是收紧而非方言化。但反过来,你的现有 JS 代码很可能因为用了数组洞、new Number、非 ASCII 大小写折叠而无法直接运行。
五、客观分析:优势与局限
优势:
- 内存预算极低:10 kB RAM / 100 kB ROM 是嵌入式场景里的硬门槛能力,自带分配器 + ROM 标准库让裸金属部署成为可能;
- 工程取向克制:不依赖 libc、不用 C 栈、解析器避免递归,C 栈占用有界,适合栈空间极小的 MCU;
- 速度不妥协:官方称与 QuickJS 相当,意味着”小”不是用慢换来的;
- MIT 协议,可自由用于商业嵌入式产品。
局限(官方 README 自己写明):
- 语法是 ES5 子集:没有完整现代 JS,数组洞、装箱、非 ASCII 折叠等被禁,现有代码移植成本真实存在;
- 字节码不做向后兼容:官方明确”不保证字节码级别向后兼容”,且字节码执行前不校验——只应运行可信来源的字节码;
- C API 心智负担高:对象会移动,长期持有
JSValue是 bug 温床,必须用 GCRef 槽位管理,比写 QuickJS 代码更小心; - 仍是 Bellard 个人/小团队项目,生态、文档与工具链远不如成熟引擎,是否长期维护需自行评估。
六、谁该关注它
- 做 IoT / 可穿戴 / 传感器固件的人:需要在 KB 级内存里跑一点配置脚本或业务逻辑,MQuickJS 是目前少有的、把 JS 引擎做到这个量级的方案;
- 想理解”极致资源约束下怎么做语言实现”的人:压缩式 GC、一字 Value、ROM 字节码、WTF-8 字符串,是一份密度极高的工程样本;
- 普通 Web/Node 开发者:不必关心——它面向裸金属,不是给你跑后端或前端用的。