MicroQuickJS can run the latest javascript (sort of)

4 min read
javascripttypescriptMicroQuickJSembedded

MicroQuickJS is a new js runtime from s-tier engineer Fabrice Bellard - if you haven’t heard of Fabrice I implore you to go through his website, be prepared for general shock and amazement at his output throughout the years.

MicroQuickJS caught my eye via this Hacker News post. It’s an embedded-friendly JS engine that runs in as little as 10kb of ram and 100kb of rom, which is smaller than Lua and some of the comments on that post echo my thoughts - I would much rather write Javascript than Lua.

I’ve been experimenting with it on and off for the first half of this year. I’ve managed to get things like XIP working so I can use old, slow-to-flash ROM and still have functioning applications.

Getting the latest JS running

A problem I’m having at the moment is that I’m limited to the mquickjs subset (roughly ES5). ES5 is 17 years old at this point and predates my exposure to the language by 11 years. So I find myself reaching for features that just don’t exist. Things like let, const, default parameters, spreading, destructuring and arrow functions are language features I use every day in my day job.

Arguably the easiest fix is to just use TypeScript to downlevel your JavaScript. Install TypeScript, create the following tsconfig and then run tsc.

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "lib": ["es6"],
        "strict": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "skipLibCheck": true,
        "outDir": "./dist",
        "rootDir": "./"
    },
    "files": ["index.ts"]
}

What we’re doing here is telling TypeScript we don’t want the dom library included - we just want the ES6 library, and to target ES5. We’ve enabled strict for the type safety, but disabled alwaysStrict so tsc doesn’t prepend “use strict”; to the output.

From my quick experiments I’ve found:

  • let and const are changed to var.
let val = 1;
const offset = 2;
val += offset;
var val = 1;
var offset = 2;
val += offset;
  • Destructured objects are changed into a series of assignments
const { a, b, c } = {
    a: 'first',
    b: 'second',
    c: 'third'
};
var _a = {
    a: 'first',
    b: 'second',
    c: 'third'
}, a = _a.a, b = _a.b, c = _a.c;
  • Arrow functions are changed into function expressions
const func = () => {
    console.log('hi');
}
var func = function () {
    console.log('hi');
};
  • Default parameters are changed into a check
function add(a = 1, b = 1) {
    return a + b;
}
function add(a, b) {
    if (a === void 0) { a = 1; }
    if (b === void 0) { b = 1; }
    return a + b;
}

Anyone who’s ever used Babel or something similar will be aware this is a standard process as part of most JS build pipelines. The difference here is we’re being more explicit in what we want in our output. We’re not targeting IE11 or anything like that, we’re targeting a super specific runtime that has a very small footprint. So we can be more explicit in what we want to use and what we don’t.

There’s two things worth noting here. First, this is a compilation step, which is somewhat counter-intuitive to what JS represents. In my case - it’s a necessary trade-off, but might not work for other use cases. Second, the downlevelled JavaScript isn’t 100% guaranteed to run. If a missing feature isn’t downlevelled correctly then you’ll get a runtime error - and possibly one which is harder to decipher than usual, as there’s no sourcemaps in MicroQuickJS land.

Final thoughts

MicroQuickJS is awesome and I’m really glad there’s developers out there making stuff like this. I’m almost certain we’ll see it crop up in various places in the future. And JS devs, there’s no better time than now to get into embedded development - now you’ve even got a new tool that’ll let you write in a language you’re already familiar with.