Microvium is very small
TL;DR: The Microvium JavaScript engine for microcontrollers takes less than 16 kB of ROM and 64 bytes of RAM per VM while idle, making it possibly the smallest JavaScript engine to date with more language features than engines 4x its size.
I’ve designed Microvium from the ground up with the intention for it to be tiny, and it’s been an absolute success in that sense. Microvium may be the smallest JavaScript engine out there, but it still packs a punch in terms of features.
*Edit: this post was originally written when Microvium was around 8.2 kB of ROM. Since then, new features have been added. As of August 2023, Microvium is now 12 kB.
Does size matter?
Size often matters in small MCU devices. A large proportion of microcontroller models available on the market still have less than 64 kB of flash and less than 2 kB of RAM. These are still used because they’re smaller, cheaper, and have lower power than their larger counterparts. All the microcontrollers I’ve worked with in my career as a firmware engineer have had ≤ 16 kB RAM.
Some might say that you shouldn’t even want JavaScript on such small devices, and certainly in some cases that would be true. But as I pointed out in my last post, juggling multiple operations in firmware can be both easier and more memory efficient if the high-level logic is described in terms of a language like JavaScript, even if that’s the only thing you’re using it for.
Even on larger devices, do you really want to dedicate a large chunk of it to a JavaScript engine? A smaller engine is a smaller commitment to make — a lower barrier to entry.
How does it compare?
If I Google “smallest JavaScript engine for microcontrollers”, the first one on the list is Elk. Elk is indeed pretty tiny. For me, it compiles to just 11.5 kB of flash1. Microvium compiled with the same settings compiles to about 12 kB — in the same ballpark.
What about RAM?
The amount of RAM Elk uses is not pre-defined — you give it a buffer of RAM of any size you want, but it needs to be at least 96 bytes for the VM kernel state. Microvium takes 36 bytes for the kernel state.
But where there’s a massive difference in memory requirement is that Elk requires all of its memory allocated upfront, and keeps it for the lifetime of the VM. If your script’s peak memory in Elk is 1 kB then you need to give it a 1 kB buffer at startup, so its idle memory usage is 1 kB. Microvium on the other hand uses malloc and free to allocate when needed and free when not needed. Its idle memory usage can be as low as 88 bytes. In typical firmware, idle memory is much more important than peak memory, as I explained in my last post.
What about the feature set? This is another area where Microvium and Elk diverge significantly. The following table shows the differences:
Microvium | Elk | |
---|---|---|
var , const (Elk supports let only) | ✓ | |
do , switch , for | ✓ | |
Computed member access a[b] | ✓ | |
Arrow functions, closures | ✓ | |
try … catch | ✓ | |
async –await | ✓ | |
Modules | ✓ | |
Snapshotting | ✓ | |
Uses intermediate bytecode (better performance) | ✓ | |
Parser at runtime | ✓ | |
ROM | 12 kB | 11.5 kB |
Idle RAM | 88 B | Lots |
Peak kernel RAM | 36 B | 96 B |
Slot size (size of simple variables) | 2 B | 8 B |
The only thing that Elk can do that Microvium can’t do is execute strings of JavaScript text at runtime. So if your use case involves having human users directly provide scripts to the device, without any intermediate tools that could pre-process the script, then you can’t use Microvium and you might want to use Elk, mJS, or a larger engine like XS. On the other hand, if your use case has at any point a place where you can preprocess scripts before downloading them to the device then you can use Microvium.
Comparing with mJS
But Cesanta, the maker of Elk, also made a larger JS engine with more features: mJS, which is probably the closest match to Microvium in terms of feature set. mJS lets you write for-loops and switch statements for example.
Since they’re closely matched for intent and features, I did a more detailed comparison of mJS and Microvium here. But here’s a summary:
Microvium | mJS | Elk | |
var , const (mJS supports let only) | ✓ | ||
Template strings | ✓ | ||
Arrow functions and closures | ✓ | ||
try … catch | ✓ | ||
async –await | ✓ | ||
ES Modules (but mJS does support a non-standard load function) | ✓ | ||
do , switch , for | ✓ | ✓ | |
Computed member access a[b] | ✓ | ✓ | |
Uses intermediate bytecode (better performance) | ✓ | ✓ | |
Some builtin-functions | ✓ | ||
Parser at runtime | ✓ | ✓ | |
ROM | 12 kB | 45.6 kB | 11.5 kB |
Slot size | 2 B | 8 B | 8 B |
I’ve lumped “some builtin-functions” into one box because it’s not a language feature as such. mJS has a number of builtin functions that Microvium doesn’t have – most notably print
, ffi
, s2o
, JSON.stringify
, JSON.parse
and Object.create
. You can implement these yourself in Microvium quite easily without modifying the engine (or find implementations online), and it gives you the option of choosing what you want rather than having all that space forced on you2.
In terms of features, mJS is a more “realistic” JavaScript engine, compared to Elk’s minimalistic approach. I wouldn’t want to write any substantial real-world JavaScript without a for-loop for example. Like Microvium, mJS also precompiles the scripts to bytecode and then executes the bytecode, which results in much better performance than trying to parse on the fly. Engines like Elk that parse as they execute also have the unexpected characteristic that comments and whitespace slow them down at runtime.
But the added features in mJS means it costs a lot more in terms of ROM space — about 4x more than Elk and Microvium.
Microvium still has more core language features than mJS, making it arguably a more pleasant language to work in. These features are actually quite useful in certain scenarios:
- Proper ES module support is important for code organization and means that your Microvium modules can also be imported into a node.js or browser environment. You can have the same algorithms shared by your edge devices (microcontrollers), backend servers, and web interfaces, to give your users a unified experience.
- Closures are fundamental to callback-style asynchronous code, as I explained in my previous post.
Conclusion
I’m obviously somewhat biased since Microvium is my own creation, but the overall picture I get is this:
- Microvium is the smallest JavaScript engine that I’m aware of3
- In this tiny size, Microvium actually supports more core language features than engines more than 4x its size. Some of these features are really useful for writing real-world JS apps.
- Having said that, Microvium has fewer built-in functions — it’s more of a pay-as-go philosophy where your upfront commitment is much less and you bring in support for what you need when you need it.
- The big trade-off is that Microvium doesn’t have a parser at runtime. In the rare case that you really need a parser at runtime, Microvium simply won’t work for you.
Something that made me smile is this note by one of the authors of mJS in a blog posts:
That makes mJS fit into less than 50k of flash space (!) and less than 1k of RAM (!!). That is hard to beat.
https://mongoose-os.com/blog/mjs-a-new-approach-to-embedded-scripting/
I have great respect for the authors of mJS and what they’ve done, which makes me all the more proud that Microvium is able to knock this out of the ballpark, beating what the seasoned professionals have called “hard to beat”. Of course, this comes with some tradeoffs (no parser and no builtin functions), but I’ve achieved my objective of making a JavaScript engine that has a super-low upfront commitment and will squeeze into the tiniest of free spaces, all while still including most of the language features I consider to be important for real-world JavaScript apps.
All of the sizes quoted in this post are when targeting the 32-bit ARM Cortex M0 using GCC with optimization for size. I’m measuring these sizes in June 2022, and of course they may change over time. ↩
The
ffi
in mJS is something that would need to be a built-in in most engines but Microvium’s unique snapshotting approach makes it possible to implement theffi
as a library just like any of the other functions ↩Please let me know if you know of a smaller JS engine than Microvium. ↩
9 Replies to “Microvium is very small”
This is awesome! About 15 years ago I tried to get a scripting language built into WD HDDs, mainly to allow the same code to be used for remote and built-in tests, with a suitable abstraction for the hardware layer. The idea was to debug using a desktop connection, then move the script to the drive.
Management had a hard time taking the project seriously when I suggested porting Pawn – you can imagine what they heard when I said that with my English accent!
How easy is it to make calls from Javascript to functions written in C or asm?
Yes, this sounds very similar to my situation. At every past job where I’ve worked as a firmware engineer, we’ve eventually reached a point where it would make things a lot cleaner if we had scriptable behavior that was shared between the embedded device and say a backend server or test environment. A concrete example is that you can pay for parking at a parking meter (which has a microcontroller) or pay for the same parking space on your phone, so you want the logic, user workflow, and tariff rules to be the same either way. If Microvium had existed when I was doing that job, I would definitely have used it. I kept running into similar problems, so I decided to create Microvium.
I think it’s pretty easy to make calls from JS into C. Have a look at the C code in the getting-started guide for an example where the JS calls
print
that is implemented in C. Let me know if anything is confusing so I can revise the guide if necessary, since one of the objectives of Microvium is to be easy to use.Nice post thanks !!!
tell me more…I’m a freak of arduino/raspberry how can I use it on SBC running on arm 64?
My suggestion is to try the getting-started guide linked from the readme
https://github.com/coder-mike/microvium
Let me know how it goes!
Thanks for the valuable information.
Can you please compare Microvium to Espruino ?
Please consider the compatibility/support for many microcontrollers, firmware OTA for the engine itself as well as for the Java script code on top of it, complete JS functionality
Off the top of my head (may need some fact-checking, and may be out of date since I haven’t looked recently) –
Microvium is just an engine (at this stage). I don’t know if Espruino gives you OTA or things like that out of the box, but Microvium leaves that to you to handle.
In terms of support for MCUs, Microvium is distributed as ISO C that will run anywhere that C will run. I don’t know about Espruino. But bear in mind that Microvium doesn’t give you any libraries or builtin support for IO or anything like that — I don’t know what espruino gives, but it’s a much more established engine so probably either Espruino or its community will have libraries and integrations for a variety of hardware (I’m guessing).
Also consider the license — Microvium is MIT licensed which is very permissive while Espruino is slightly less permissive, depending on what you want to use it for.
My take is this: if you can afford the massive memory footprint of Espruino, consider going with Moddable’s XS engine instead since it gives full modern ECMAScript and I think performance will be more predictable. If can’t afford the footprint, Microvium has the most features at its size.
Thanks a lot for the details. very well apprciated.
Can you please elaborate on the not having/providing any libraries or builtin support for IO with some possible examples ?
I/O in Microvium is pretty simple – just
vmExport
andvmImport
to hook into the host (firmware). If you follow the getting-started guide you pretty much know the whole I/O interface for Microvium. If you want to access a GPIO for example, you can just have a C function that does it (or use any off-the-shelf C library) and then usevmImport
to access it.On the other hand, Espruino has a whole reference manual for various IO functionality. For example, digitalRead to read the value of I/O.
The trade-off depends on your situation. I personally think the Microvium approach is better for professionals and the Espruino approach is better for hobbyists. That is, Espruino allows a hobbyist to buy an off-the-shelf official board or third-party board for making a hobby project and you can get running on it almost instantly, but then if you want to use the Espruino engine in a real IoT project with custom hardware then it’s going to be a lot of work.
Whereas Microvium is designed from the beginning to fit into custom hardware. If you have an IoT project, you should be able to get Microvium hello-world running on it in a couple of hours (depending how familiar you are with it already) whereas I wouldn’t be surprised if it took a week to get hello-world running with Espruino on your custom hardware.
Here I asked ChatGPT-4 how long it would take to get Espruino working on TM4C123G dev board assuming no existing drivers for that board (take the answer with a pinch of salt, as with anything from ChatGPT):
Full answer from ChatGPT here. Note that ChatGPT here is assuming that you need to implement the drivers for all the Espruino IO just to get “Hello, World” working, which I don’t know is true or not.