Micro QuickJS:Fabrice Bellard 把 JS 引擎压进 10 KB 内存的嵌入式实验

JavaScript
嵌入式
QuickJS
Fabrice Bellard
开源
2026/10/15
·

阅读时间: 大约 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 做一手拆解。

bellard.org/mquickjs 官方项目分发页:核心源码之外,另以 mquickjs-extras.tar.xz(约 6.1 MB,2025-12-26 更新)提供附加测试与打过补丁的 Octane 基准

一、它解决的是什么问题

普通 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 也含标准库,具体随裁剪策略浮动。

mquickjs 官方 GitHub README 节选:恒为 stricter mode、追踪式垃圾回收、VM 不使用 CPU 栈、字符串存为 UTF-8;REPL 为 mqjs,可用 --memory-limit 10k 跑 tests/mandelbrot.js

三、技术机制:为什么能这么小

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 大小写折叠而无法直接运行。

五、客观分析:优势与局限

优势:

  1. 内存预算极低:10 kB RAM / 100 kB ROM 是嵌入式场景里的硬门槛能力,自带分配器 + ROM 标准库让裸金属部署成为可能;
  2. 工程取向克制:不依赖 libc、不用 C 栈、解析器避免递归,C 栈占用有界,适合栈空间极小的 MCU;
  3. 速度不妥协:官方称与 QuickJS 相当,意味着”小”不是用慢换来的;
  4. MIT 协议,可自由用于商业嵌入式产品。

局限(官方 README 自己写明):

  1. 语法是 ES5 子集:没有完整现代 JS,数组洞、装箱、非 ASCII 折叠等被禁,现有代码移植成本真实存在;
  2. 字节码不做向后兼容:官方明确”不保证字节码级别向后兼容”,且字节码执行前不校验——只应运行可信来源的字节码;
  3. C API 心智负担高:对象会移动,长期持有 JSValue 是 bug 温床,必须用 GCRef 槽位管理,比写 QuickJS 代码更小心;
  4. 仍是 Bellard 个人/小团队项目,生态、文档与工具链远不如成熟引擎,是否长期维护需自行评估。

六、谁该关注它

  • 做 IoT / 可穿戴 / 传感器固件的人:需要在 KB 级内存里跑一点配置脚本或业务逻辑,MQuickJS 是目前少有的、把 JS 引擎做到这个量级的方案;
  • 想理解”极致资源约束下怎么做语言实现”的人:压缩式 GC、一字 Value、ROM 字节码、WTF-8 字符串,是一份密度极高的工程样本;
  • 普通 Web/Node 开发者:不必关心——它面向裸金属,不是给你跑后端或前端用的。

参考来源