<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 杰杨</title>
    <description>The latest articles on DEV Community by 杰杨 (@_651fb917ec431e2f46f006).</description>
    <link>https://dev.to/_651fb917ec431e2f46f006</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2388611%2F002ee9b5-75a0-4981-8de2-c0429d260be4.png</url>
      <title>DEV Community: 杰杨</title>
      <link>https://dev.to/_651fb917ec431e2f46f006</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_651fb917ec431e2f46f006"/>
    <language>en</language>
    <item>
      <title>Why I built a 6MB JavaScript runtime for embedded and IoT devices</title>
      <dc:creator>杰杨</dc:creator>
      <pubDate>Sat, 10 Oct 2026 15:16:53 +0000</pubDate>
      <link>https://dev.to/_651fb917ec431e2f46f006/why-i-built-a-6mb-javascript-runtime-for-embedded-and-iot-devices-40kg</link>
      <guid>https://dev.to/_651fb917ec431e2f46f006/why-i-built-a-6mb-javascript-runtime-for-embedded-and-iot-devices-40kg</guid>
      <description>&lt;p&gt;I love Node.js. It's why I can ship a backend in an afternoon.&lt;/p&gt;

&lt;p&gt;But I also write firmware and deal with edge hardware — OpenWrt routers, MIPS&lt;br&gt;
boards, small ARM gateways. And on that class of device, Node.js is often just&lt;br&gt;
too much: a 100 MB+ runtime, high memory use, and a cold start you can feel.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;xuan (玄)&lt;/strong&gt; — a lightweight JavaScript runtime for IoT and embedded&lt;br&gt;
devices. It's built on &lt;a href="https://bellard.org/quickjs/" rel="noopener noreferrer"&gt;QuickJS&lt;/a&gt; and forked from&lt;br&gt;
&lt;a href="https://github.com/saghul/txiki.js/" rel="noopener noreferrer"&gt;tjs / txiki.js&lt;/a&gt;. It's MIT licensed and on&lt;br&gt;
GitHub here: &lt;strong&gt;&lt;a href="https://github.com/topelinux/xuan" rel="noopener noreferrer"&gt;https://github.com/topelinux/xuan&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is about why it exists, how it's put together, and how to try it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem with "just use Node"
&lt;/h2&gt;

&lt;p&gt;On a server, none of Node's costs matter. On a $10 board with 64 MB of RAM, they&lt;br&gt;
all do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binary size&lt;/strong&gt; — you don't want a 100 MB runtime on a device with a small
flash partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — resident memory is at a premium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup&lt;/strong&gt; — if your program runs occasionally (a cron-like sensor task, a
request handler on a gateway), cold start dominates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to run &lt;em&gt;JavaScript&lt;/em&gt; — and ideally a good chunk of the npm ecosystem — in&lt;br&gt;
that environment, with a runtime measured in single-digit megabytes.&lt;/p&gt;
&lt;h2&gt;
  
  
  What xuan is
&lt;/h2&gt;

&lt;p&gt;xuan embeds &lt;strong&gt;QuickJS&lt;/strong&gt; (a small, fast ES2020+ engine by Fabrice Bellard) and&lt;br&gt;
adds the pieces a real application needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Node.js-compatible API layer&lt;/strong&gt; (so many existing npm packages run with
little or no change)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;built-in IoT modules&lt;/strong&gt;: MQTT, serial port, and FFI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;standalone compilation&lt;/strong&gt;: turn a script into a single self-contained binary&lt;/li&gt;
&lt;li&gt;cross-compilation to &lt;strong&gt;x86, ARM, MIPS and RISC-V&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a fork of txiki.js, which already had an excellent QuickJS + Node-compat&lt;br&gt;
foundation. My focus is the IoT/embedded angle and the built-in device modules.&lt;/p&gt;
&lt;h2&gt;
  
  
  Built-in modules, not npm-installed ones
&lt;/h2&gt;

&lt;p&gt;On constrained devices, pulling native modules at runtime is painful. So the&lt;br&gt;
ones you actually need are built in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MQTT&lt;/strong&gt; — publish/subscribe, the lingua franca of IoT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SerialPort&lt;/strong&gt; — talk to MCUs and modems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFI&lt;/strong&gt; — call into C libraries directly, so you can reuse existing code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPIO / I2C / SPI&lt;/strong&gt; — in progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal MQTT client looks like normal Node code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mqtt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mqtt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mqtt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mqtt://broker.emqx.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot/device/001/#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;qos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot/device/001/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;qos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And FFI lets you reach for C when you need to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FFI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tjs:ffi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;libc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;FFI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FFI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Lib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LIBC_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;atoi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;FFI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;atoi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;FFI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;FFI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;atoi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Standalone binaries with &lt;code&gt;xuan compile&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You can also embed your app &lt;em&gt;into&lt;/em&gt; the runtime and ship one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./build/xuan compile app.js myapp
./myapp        &lt;span class="c"&gt;# no Node.js, no runtime install required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the deployment story I wanted for devices: build once on your machine,&lt;br&gt;
copy one binary onto the target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rough numbers
&lt;/h2&gt;

&lt;p&gt;On a MacBook Air M1 (executable size and cold start only — this is &lt;em&gt;not&lt;/em&gt; a&lt;br&gt;
resident-memory benchmark):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Cold start (mean)&lt;/th&gt;
&lt;th&gt;Binary size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;xuan&lt;/td&gt;
&lt;td&gt;5.37 ms&lt;/td&gt;
&lt;td&gt;6.2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bun&lt;/td&gt;
&lt;td&gt;8.82 ms&lt;/td&gt;
&lt;td&gt;58 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;20.29 ms&lt;/td&gt;
&lt;td&gt;112 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;220 runs each, randomly interleaved, &lt;code&gt;-e "0"&lt;/code&gt;, wall-clock from process start to&lt;br&gt;
exit. Treat it as a rough data point, not a victory lap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those numbers are on a fast desktop CPU; the interesting part for embedded folks&lt;br&gt;
is the &lt;em&gt;shape&lt;/em&gt; — a single-digit-megabyte runtime with millisecond startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's put together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│           xuan Runtime              │
├─────────────────────────────────────┤
│  Node.js Compatible API Layer       │
├─────────────────────────────────────┤
│  IoT Native Modules (MQTT/Serial/FFI)│
├─────────────────────────────────────┤
│  QuickJS Engine                     │
├─────────────────────────────────────┤
│  Embedded System (Linux/RTOS)       │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript sources are bundled (esbuild) and compiled into C, then embedded into&lt;br&gt;
the binary. Local changes to the bundled dependencies (libwebsockets, QuickJS,&lt;br&gt;
libuv, mimalloc) are kept as patches and applied at build time, so a clean clone&lt;br&gt;
builds reproducibly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and run
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/topelinux/xuan.git
&lt;span class="nb"&gt;cd &lt;/span&gt;xuan
git submodule update &lt;span class="nt"&gt;--init&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;      &lt;span class="c"&gt;# esbuild etc. — needed before make&lt;/span&gt;
make             &lt;span class="c"&gt;# -&amp;gt; ./build/xuan&lt;/span&gt;

./build/xuan run app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What works, and what's next
&lt;/h2&gt;

&lt;p&gt;Working today: the Node-compat layer, MQTT, serial port, FFI, &lt;code&gt;xuan compile&lt;/code&gt;,&lt;br&gt;
and cross-compilation. Next up: GPIO, I2C and SPI, plus widening Node API&lt;br&gt;
coverage.&lt;/p&gt;

&lt;p&gt;It's early, and I'd genuinely value feedback — especially from people shipping&lt;br&gt;
on constrained hardware. Which boards, which Node APIs, which device modules&lt;br&gt;
would you need first?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/topelinux/xuan" rel="noopener noreferrer"&gt;https://github.com/topelinux/xuan&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Credit where it's due: the foundation is &lt;a href="https://github.com/saghul/txiki.js/" rel="noopener noreferrer"&gt;txiki.js&lt;/a&gt;&lt;br&gt;
by Saúl Ibarra Corretgé, and the engine is &lt;a href="https://bellard.org/quickjs/" rel="noopener noreferrer"&gt;QuickJS&lt;/a&gt;&lt;br&gt;
by Fabrice Bellard.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>iot</category>
      <category>embedded</category>
    </item>
  </channel>
</rss>
