Small & embedded engines
Learn how Hermes, QuickJS, XS, and engine262 choose startup, memory, speed, and specification clarity for different JavaScript jobs.
- 01Match an engine to a needExplain why a phone app, a microcontroller, a server, and spec testing each need a different kind of engine.
- 02Explain prepared bytecodeDescribe how Hermes can make compact bytecode when the app is built, and why that helps a React Native app start fast.
- 03Choose by purpose, not only speedCompare QuickJS, XS, and engine262 without calling one engine the best for every job.
Why make a small engine?
A JavaScript engine is the program that reads your JavaScript and runs it. Browsers have very big engines, because they run the whole web. Some jobs need a smaller engine:
- A phone app should open quickly.
- A tiny device has very little memory.
- A team that writes the language rules wants to see each rule clearly.
A small engine is a JavaScript engine built for one main need, such as a fast start, low memory use, fitting inside another program, or checking the language rules. It still runs JavaScript. It just gives up some things to be good at its one need.
No engine is best for every job. A server runs for days, so it can wait for repeated code to get faster. A tiny device can't wait or spend extra memory. A team checking the language rules cares more about seeing each step than about speed.
function addCartPrice(price, times) { let total = 0; for (let count = 0; count < times; count += 1) total += price; return total;} console.log(addCartPrice(3, 4));Line 1 makes a function called addCartPrice. Line 2 starts total at zero. Line 3 is a loop: it adds the price once for each item. Line 7 calls the function with a price of 3 and four items, so it prints 12.
Two kinds of engine run this code in different ways:
- An interpreter runs the steps the same way every time. Its speed stays steady.
- A JIT engine first watches the loop repeat. Then it spends extra work to make a faster version. This early extra work is called warm-up.
Neither one is better in general. The right choice depends on what the product needs most.
A restaurant kitchen can cook many dishes at once. A camping stove is light. It is ready almost at once, and it fits in a backpack. It cooks a simple meal more slowly. That is fine, because on a hike you need something light, not a full kitchen.
- In real life: A camping stove fits in a backpack
- In JavaScript: A small engine fits a product with little room
- In real life: It is ready to cook almost at once
- In JavaScript: It starts quickly
- In real life: It cooks one simple meal
- In JavaScript: It does one main job well
- In real life: A big kitchen cooks many dishes at once
- In JavaScript: A big JIT engine makes long-running code fast
This lesson comes after Inside Deno, Bun & edge runtimes. That lesson compared runtimes. This one looks at the engines inside, and at what each engine chooses to keep or leave out.
A steady interpreter and a warming JIT
An interpreter reads the program one step at a time and does each step. A JIT compiler (JIT means "just in time") turns code that runs often into fast machine code while the program is running. Machine code is the language the computer's chip runs directly.
function addPrice(price) { return price + 1;}console.log(addPrice(3));Line 1 makes a tiny function. Line 2 adds one to price. Line 4 calls it with 3, so it prints 4.
An interpreter does this addition the same way on every call. A JIT works in three stages:
- It first runs a plain, general version of the function.
- It watches many calls and notes facts, such as
priceis always a number. - It then makes a faster version that uses those facts.
This helps code that runs many times. It also costs something: work at the start, memory for the new machine code, and memory for the notes.
So "fast to start" and "fast after a while" are two different things. Opening an app once is not like running a busy server all day. Always ask which moment the user feels.
The table below compares four kinds of engine. Read each row as one kind of engine. Each score goes from 1 to 10, and a bigger number is better for that column.
| Kind of engine | Start score | Speed after warm-up score | Memory score | Where it fits best |
|---|---|---|---|---|
| JIT engine | 4 | 10 | 8 | Server |
| AOT bytecode interpreter | 9 | 6 | 5 | Phone app |
| Small interpreter | 8 | 4 | 9 | Microcontroller with 256 KB of memory |
| Spec interpreter | 3 | 1 | 3 | Spec testing |
These scores are made up on purpose. They give us simple words for comparing engines. They are not real times or real memory sizes.
Hermes and prepared bytecode
Hermes is a JavaScript engine made so that React Native apps start fast. React Native is a tool for building phone apps with JavaScript.
Before an engine can run JavaScript, it turns the text into bytecode. Bytecode is a list of small, simple instructions that the engine can run. Most engines make the bytecode when the app opens. Hermes can make it earlier, when the app is built. Doing work early like this is called ahead-of-time, or AOT. The Hermes project says it does two things:
- It improves the code ahead of time, while the app is built.
- It stores the result as compact (small) bytecode.
const appBytecode = "prepared at build time"; function openApp() { console.log(appBytecode);} openApp();Line 1 is only a text label for this lesson. It is not real Hermes bytecode. Line 3 makes a function called openApp. Line 4 prints the label. Line 7 calls the function, so the code prints prepared at build time.
Chopped vegetables let you start cooking sooner, because the cutting was done before dinner. Hermes does the same with bytecode. Turning JavaScript into bytecode can happen when the app is built, before anyone installs it.
- In real life: The vegetables are chopped before dinner
- In JavaScript: The bytecode is made when the app is built
- In real life: Cooking starts sooner
- In JavaScript: The app skips some work when it opens
- In real life: The cook still follows the recipe steps
- In JavaScript: The engine still runs the bytecode
- In real life: A helper did the chopping, not the cook
- In JavaScript: The build tools made the bytecode, not the phone
This does not mean "Hermes never compiles". The Hermes project only says it improves code ahead of time and uses compact bytecode. The exact details depend on the Hermes version and on how React Native uses it. To know how fast a real app opens, measure it with real tools.
You may also read about Static Hermes. It is a separate project that turns code into machine code. It is not the usual bytecode path that React Native uses. Check which version you use before you make claims about it.
The main idea is simple. Moving work from "when the app opens" to "when the app is built" can make the app open faster. The bytecode file you ship then becomes part of how the app runs.
QuickJS: a small engine inside other programs
QuickJS calls itself a small engine that is easy to embed. To embed an engine means to put it inside another program, so that program can run JavaScript too. The QuickJS project lists these features:
- A fast interpreter.
- A very quick start.
- It turns JavaScript into bytecode before running it.
- It frees memory with reference counting and cycle removal. Both are explained below.
const order = { item: "tea" };const anotherName = order; console.log(anotherName.item);Line 1 makes one object and calls it order. Line 2 gives the same object a second name, anotherName. It does not copy the object. Line 4 reads the item through anotherName, so it prints tea.
order and anotherName point to the same object. Reference counting keeps a number on the object: how many names point to it. Here the number is 2.A reference is a name or property that points to an object. Reference counting means the engine keeps a number on each object: how many references point to it. When a reference goes away, the number goes down. When it reaches zero, nothing can use the object, so the engine frees its memory.
A cycle is a problem for counting. Picture two objects that point to each other. Nothing else points to them, so your code can never use them again. But each one still has a count of 1, from the other object. The counts never reach zero.
That is why QuickJS also lists cycle removal: an extra step that finds such loops and frees them. Counting alone can't do it. To see the counting step by step, read Reference counting & cycles.
Embedding helps when a bigger program needs JavaScript for one small feature. There, a small size and a simple way to connect matter more than top speed after minutes of warm-up.
You may also see QuickJS-NG. It is a separate project that started as a copy of QuickJS. It is not a new name for the original. Both are easy to embed. Check each project's own feature list, because they release new versions at different times.
XS on microcontrollers
A microcontroller is a tiny computer on one chip. It runs things like a smart light, a thermostat, or a sensor. It often has very little memory.
XS is the JavaScript engine inside the Moddable SDK. An SDK is a set of tools for building programs for one kind of device. The Moddable documents say XS is made for devices with few resources (little memory and a slow chip). They describe:
- Support for microcontroller chips.
- JavaScript modules that are prepared before they reach the device.
- Design choices for devices with few resources.
const reading = 23;const message = "sensor: " + reading; console.log(message);Line 1 stores a sensor reading, 23. Line 2 builds a short message. Line 4 prints it, so the output is sensor: 23.
On a microcontroller, the whole product shares one fixed amount of memory. Your data, the screen, the network, the device's own code, and JavaScript all fit in it together. So a feature that is harmless on a laptop may not fit on the device at all.
This does not mean every XS program is tiny. It means the device needs careful choices about memory. The Moddable tools help with that: they can prepare modules ahead of time and build the JavaScript into the device's program.
When you compare engines for a device, ask these questions:
- Which device features can JavaScript use?
- How is the code prepared before it reaches the device?
- How much memory does each part get?
- How do you update the device later?
Which JavaScript features an engine supports is only one part of the choice.
There are other small engines too. Duktape is a small engine you can embed in C programs; it supports only some JavaScript features newer than ES5 (the 2009 version). Espruino is an interpreter for microcontrollers with a live console, where you type code and see the result at once. Each one has its own device features and language support, different from XS.
engine262 follows the language rules closely
The rules of JavaScript are written in one official document called ECMA-262. People also call it the specification, or spec for short.
engine262 is an engine that follows ECMA-262. It is itself written in JavaScript. Its project lists three goals:
- Follow the spec exactly.
- Let people look inside and see each step as it runs.
- Be easy to change.
The project also says that speed is not a goal if it would hurt those three goals.
const price = 3;const count = 2; console.log(price + count);Line 1 makes price. Line 2 makes count. Line 4 adds them, so it prints 5.
We call an engine like this a spec interpreter. It helps when the question is "what should this JavaScript mean?" A team can try a new rule in engine262 and watch it work. That is easier than changing a big, fast engine first. The engine262 project says it has also helped find mistakes in ECMA-262 and in test262 (the official set of JavaScript tests).
A teacher can cook a recipe word by word to check that the recipe itself makes sense. It is slow on purpose. engine262 does this for the language rules. It helps people check and try out the rules. It is not meant to serve a busy restaurant.
- In real life: A teacher follows the recipe word by word
- In JavaScript: engine262 follows each step of the spec
- In real life: The teacher checks that the recipe makes sense
- In JavaScript: The engine helps people check the language rules
- In real life: The meal takes longer
- In JavaScript: Speed is not the goal
- In real life: The teacher can try a changed recipe
- In JavaScript: A team can try a new rule
Use a spec interpreter to learn, test, and talk about what JavaScript code means. Use a big, fast engine when you ship a real app that must run well on real devices.
Two more names you may meet:
- Boa is another engine you can embed. It is written in the Rust language and is tested against the spec. It is a separate project from engine262.
- test262 is the official set of JavaScript tests. It is not an engine. How many tests an engine passes depends on which features you count and which engine version you test.
A tiny bytecode interpreter
You met bytecode in the Hermes section: a list of small, simple instructions that an engine runs one by one. Real engines use their own bytecode, which is private and much bigger. This lesson uses a made-up bytecode with only three instructions:
pushputs a number on top of the stack. A stack is a pile of values where the newest value sits on top.addtakes the top two values off the stack and puts their sum back on top.printreads the top value and prints it. The value stays on the stack.
const program = [ { op: "push", value: 2 }, { op: "push", value: 3 }, { op: "add" }, { op: "print" },]; console.log(runBytecode(program).printed[0]);Line 1 starts the list of instructions, called program. Lines 2 and 3 push 2 and then 3. Line 4 adds them. Line 5 prints the result. Line 8 runs the program with the lesson's interpreter, runBytecode, and logs the first printed value. So it prints 5.
add takes the top two values and puts back their sum. print reads the top value, 5.Step through the lesson's tiny bytecode interpreter. It is a small model for learning, not a view inside a real engine.
script
{ op: "push", value: 2 }, { op: "push", value: 3 }, { op: "add" }, { op: "print" },]; console.log(runBytecode(program).printed[0]);The step-through above calls the same runBytecode function that the lesson's tests use. It is a small model for learning, not a view inside a real engine. Watch the stack: first 2, then 2 and 3, then 5. Then print reads 5.
Short bytecode like this suits small interpreters. They can run it without turning every repeated step into machine code. The cost is that they are usually slower than a good JIT after a long warm-up.
Choose for the device
To choose an engine, start with what the product needs most, not with an engine's name. The next small model picks from four simple kinds of engine. Its scores are made up. Never use them as real speed results.
const device = "phone";const result = chooseEngine(device); console.log(result.profile.label);Line 1 picks the phone. Line 2 asks the model, with the chooseEngine function, which kind of engine fits. Line 4 prints the name of that kind. For a phone, this model prints Bytecode interpreter with AOT bytecode.
const device = "phone";const result = chooseEngine(device); console.log(result.profile.label);Phone appThe app should open quickly, without much extra work at the start.
Bytecode interpreter with AOT bytecodeThe bytecode is made when the app is built, so the phone skips that work when the app opens.
start 4 / speed 10 / memory 8It spends extra memory and warm-up work so code that repeats runs fast.
start 9 / speed 6 / memory 5The bytecode is made when the app is built, so the phone skips that work when the app opens.
start 8 / speed 4 / memory 9A small engine leaves more of the device's little memory for the product.
start 3 / speed 1 / memory 3It shows each step of the language rules clearly. Speed does not matter here.
The model's answer. Phone app: The bytecode is made when the app is built, so the phone skips that work when the app opens.
Step through the lesson's chooser. Its scores are made up, to show that a fast start, speed, and low memory pull in different directions.
script
const result = chooseEngine(device); console.log(result.profile.label);In the playground, change only one thing: the device. The reset button goes back to the phone app. Read the reason first, then the made-up scores. Each device needs something different:
- A phone app needs a fast start.
- A microcontroller needs to use little memory.
- A server needs top speed after warm-up.
- Spec testing needs to see each step clearly.
The table below lists real facts from each project's own documents. Read each row as one engine. It has no made-up scores.
| Engine | Where is it used? | What does its project say it does? | What does that mean for you? |
|---|---|---|---|
| Hermes | React Native phone apps | It improves code ahead of time and stores it as compact bytecode. | A fast app start is its main goal. |
| QuickJS | Inside other programs | A bytecode interpreter that frees memory with reference counting and cycle removal. | It aims to be small and easy to put inside another program. |
| XS | The Moddable SDK, for small devices | A JavaScript engine for devices with little memory and a slow chip. | Its tools can prepare modules before they reach the device, to save memory. |
| engine262 | Work on the JavaScript rules (ECMA-262) | An engine written in JavaScript that follows ECMA-262. | It puts following the spec, showing each step, and being easy to change before speed. |
Questions to ask before you choose
Most app teams rarely change their engine. Many other things often decide the engine before any speed test does:
- the framework the team uses,
- the device features JavaScript needs to reach,
- how the app is kept safe,
- the debugger,
- links to code written in other languages, such as C,
- and how the app is shipped to users.
So the useful skill is knowing which questions to ask:
- Which moment matters most: the first start, code that runs over and over for hours, or a tiny amount of memory?
- Which JavaScript features and device features does the product really need?
- How will the team measure start time, memory, and how quickly the app responds, on the real device?
- Does the job need a big engine for a real product, a small engine inside another program, or a spec interpreter for trying rules?
Measure on the device your users own. A result on a server does not tell you about a phone. A phone simulator on your laptop does not tell you about a microcontroller. A famous engine name does not tell you how much memory you have.
When you can, keep the choice easy to undo. Write a small test program that works like your product. Record how long it takes to start and how much memory it uses. Then run it on the real device. A clear need gives a useful comparison. A vague question like "which is fastest?" gives a vague answer.
- A phone app opens from bytecode made when the app was built.
- A React Native app uses Hermes.
- In this lesson's model, a device has only 256 KB of memory.
- A program written in C needs a small JavaScript engine inside it.
- A team wants to try a new language rule and watch it work.
- A team checks a question about ECMA-262 or test262, the official tests.
Put each situation under the need that matters most for it. These cards help you practice thinking. They are not engine advice for every case.
Common misconceptions
- "Small means old JavaScript." Small describes what the engine is built for. It says nothing about which version of JavaScript it runs.
- "AOT bytecode is machine code for every chip." It is bytecode made early, for an interpreter to run. It makes no promise about every kind of chip.
- "Reference counting can never handle cycles." Counting alone can't. That is why QuickJS adds a cycle removal step.
- "A spec interpreter should win a speed test." engine262 says speed is not a goal if it would hurt its three goals.
- "The chooser scores are facts." They are made-up scores for learning only.
The table below compares words that sound alike. Read each row as one word: what it means, and what it does not mean.
| Word | What does it mean? | What does it not mean? |
|---|---|---|
| Small engine | An engine built for one main need, such as a fast start or low memory | An engine that can't run modern JavaScript |
| AOT bytecode | Bytecode made early, when the app is built | Machine code for every kind of chip |
| Reference counting | Counting how many references point to each object (QuickJS adds a cycle removal step) | A promise that every engine frees memory the same way |
| Spec interpreter | A tool for checking and trying out what JavaScript code should do | An engine built to win speed tests for real apps |
Practice exercises
What does this code print? Guess first, then run it.
function addCartPrice(price, times) {
let total = 0;
for (let count = 0; count < times; count += 1) total += price;
return total;
}
console.log(addCartPrice(3, 4));It prints 12. The loop turns four times, and each turn adds 3 to total: 3, 6, 9, 12.
What does this small stack program print?
const stack = [];
stack.push(2);
stack.push(3);
stack.push(stack.pop() + stack.pop());
console.log(stack[0]);It prints 5. The code pushes 2 and 3. Then it takes both values off, adds them, and pushes the sum, 5. The last line prints that 5.
Type the name of the kind of engine the model picks for a phone app.
const phoneChoice = "Bytecode interpreter with AOT bytecode";
console.log(phoneChoice);The model picks Bytecode interpreter with AOT bytecode. It stands for making the bytecode early, before the phone app opens.
Which way of freeing memory do the QuickJS documents name?
QuickJS uses reference counting with cycle removal. The cycle step matters because two objects that point only to each other keep their counts above zero forever.
A team is building the program for a small device. What should it ask first, before it chooses an engine?
Ask what matters most: startup, memory, speed, or seeing each step. That tells you which kind of engine to look at before you compare any.
A team that writes JavaScript's rules wants to try a new rule and watch it work. Which engine in this lesson fits that job?
Use engine262. Its goals are to follow ECMA-262 exactly, let people see each step, and be easy to change.
Check your understanding
For each question, first ask what the job needs most. Keep the made-up model scores apart from the real facts about each project.
Question 1 of 7What does the warm-up function print?
Read the code, then predictfunction addCartPrice(price, times) { let total = 0; for (let count = 0; count < times; count += 1) total += price; return total; } console.log(addCartPrice(3, 4));Choose an answer to see the explanation.
Question 2 of 7Why can Hermes help a React Native app start quickly?
Choose an answer to see the explanation.
Question 3 of 7Which sentence matches what the QuickJS documents say?
Choose an answer to see the explanation.
Question 4 of 7What does this small stack program print?
Read the code, then predictconst stack = []; stack.push(2); stack.push(3); stack.push(stack.pop() + stack.pop()); console.log(stack[0]);Choose an answer to see the explanation.
Question 5 of 7What is engine262 for?
Choose an answer to see the explanation.
Question 6 of 7In the made-up chooser, which device gets the small interpreter?
Choose an answer to see the explanation.
Question 7 of 7What should a team ask first when it chooses an engine?
Choose an answer to see the explanation.
Key takeaways
- A small engine is built for one main need: a fast start, low memory use, fitting inside another program, or checking the language rules.
- Hermes makes compact bytecode ahead of time, when the app is built, so React Native apps start fast.
- QuickJS is small and easy to embed. It uses bytecode, and it frees memory with reference counting plus cycle removal.
- XS is the engine in the Moddable SDK, made for small devices with little memory.
- engine262 cares more about following the spec, showing each step, and being easy to change than about speed.
- Choose from what the product needs most. Then measure on the real device.
Remember the one-liner.
A small engine is not simply slower or worse. It spends its small budget of start time, memory, and size on a different job.
Coming next: JavaScript & WebAssembly. There, JavaScript works with WebAssembly, a compact kind of program file that runs the same way on many devices.