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 constraintExplain why phone apps, microcontrollers, servers, and specification work ask for different engine designs.
- 02Explain prepared bytecodeDescribe how Hermes can prepare compact bytecode at build time, and why that helps React Native startup.
- 03Separate speed from purposeCompare QuickJS, XS, and engine262 without treating one engine as best for every job.
Why make a small engine?
A JavaScript engine turns source code into behavior. A browser engine serves a huge web platform, but some jobs need a smaller tool: a phone app wants a quick start, a tiny device has little memory, and a language team wants behavior that is easy to inspect.
A small engine is a JavaScript engine designed around a narrow constraint such as startup, memory, embedding, or checking the specification. It is not a lesser version of JavaScript; it makes different trade-offs.
There is no universal winner. A server can wait for repeated code to become fast. A device with a small memory budget cannot. A specification experiment needs a clear explanation of each rule more than a fast loop.
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 creates addCartPrice. Line 2 starts total at zero. Line 3 adds the price once for each item. Line 7 calls the function with price 3 and four items, so it prints 12.
An interpreter-only engine can run this same function steadily every time. A just-in-time, or JIT, engine may spend warm-up work learning that the loop repeats and later run it faster. That does not make either engine better without knowing the product's constraint.
A full restaurant kitchen can prepare many dishes at once. A camping stove is light, starts quickly, and fits in a backpack; it makes a simpler meal more slowly, and that is exactly the point.
- In real life: A camping stove fits in a backpack
- In JavaScript: A small engine fits a constrained product
- In real life: It starts with little setup
- In JavaScript: Startup work stays small
- In real life: It cooks a simple meal
- In JavaScript: It focuses on a narrow job
- In real life: A full kitchen handles more at once
- In JavaScript: A large JIT can optimize longer workloads
Where the analogy stops: Engine size is not literally a stove size. Real performance depends on code, device, host APIs, and memory limits.
This lesson follows Inside Deno, Bun & edge runtimes. It looks below a runtime and asks what each engine deliberately leaves in or out.
A steady interpreter and a warming JIT
An interpreter reads a representation of a program and performs its instructions. A JIT compiler translates frequently used code while the program runs, aiming for more peak speed after it has observed real behavior.
function addPrice(price) { return price + 1;}console.log(addPrice(3));Line 1 creates a tiny function. Line 2 adds one to price. Line 4 calls it with 3, so the visible output is 4.
An interpreter can execute those operations directly on every call. A JIT can first interpret or compile a general version, collect facts during many calls, then compile a specialized version. The gain is for repeated work; the cost is startup work, generated code, and bookkeeping.
That distinction explains why startup and peak speed are separate columns. Opening an app once is different from processing a busy server route all day. Always ask which moment a user feels.
| Engine model | Startup score | Peak speed score | Memory score | Typical fit |
|---|---|---|---|---|
| JIT engine | 4 | 10 | 8 | Server |
| AOT bytecode interpreter | 9 | 6 | 5 | Phone app |
| Small interpreter | 8 | 4 | 9 | 256 KB microcontroller |
| Spec interpreter | 3 | 1 | 3 | Spec testing |
The scores above are deliberately made up. A larger number means better for that one column in this model. They give us a simple language for choosing, not a claim about actual milliseconds or megabytes.
Hermes and prepared bytecode
Hermes is a JavaScript engine optimized for fast startup of React Native apps. Its official project describes ahead-of-time static optimization and compact bytecode. The useful idea is that some preparation can happen while an app is built instead of when a user opens it.
const appBytecode = "prepared at build time"; function openApp() { console.log(appBytecode);} openApp();Line 1 is a teaching label, not real Hermes bytecode. Line 3 creates openApp. Line 4 logs the prepared label. Line 7 calls the function, so the code prints prepared at build time.
Pre-chopped vegetables let cooking begin sooner because the cutting was done before dinner. Hermes's prepared bytecode has the same one idea: parsing and compiling to bytecode can happen at build time, before the app is installed.
- In real life: Vegetables are chopped before cooking
- In JavaScript: App source is prepared at build time
- In real life: Cooking starts sooner
- In JavaScript: The app avoids some startup preparation
- In real life: The cook still follows steps
- In JavaScript: The engine still executes bytecode
- In real life: The recipe was chosen earlier
- In JavaScript: Build tools choose the artifact
Where the analogy stops: Bytecode is not food, and preparation does not remove every startup cost such as loading the app and initializing native code.
Do not turn that into “Hermes never compiles.” The documented claim is about ahead-of-time static optimization and compact bytecode. Exact behavior depends on the Hermes version and the React Native integration, so use product tools for a real app measurement.
For a reader, the design lesson is simple: moving work from startup to build time can improve the first moment of an app. It also makes the shipped artifact part of the runtime plan.
QuickJS: small and embeddable
QuickJS describes itself as a small, embeddable JavaScript engine with a fast interpreter and very low startup time. It compiles JavaScript sources to bytecode. Its memory management uses reference counting with cycle removal.
const order = { item: "tea" };const anotherName = order; console.log(anotherName.item);Line 1 creates one order object. Line 2 gives that same object a second name. Line 4 reads through anotherName, so the result is tea.
Reference counting means a runtime can track how many references point to an object. When ordinary references go away, a count can fall. A cycle needs extra work because two objects may keep each other's counts above zero even when nothing else can reach them.
QuickJS documents cycle removal alongside reference counting. That phrase matters: reference counting alone has a known cycle problem, and a production implementation needs a plan for it. Continue with Reference counting & cycles for the algorithm in small steps.
Embedding means a larger native program can host JavaScript for a focused feature. The engine's small footprint and direct integration story can matter more than maximizing a hot loop after minutes of warm-up.
XS on microcontrollers
XS is the JavaScript engine in the Moddable SDK. Moddable documents XS for resource-constrained targets and documents support around microcontroller platforms, precompiled JavaScript modules, and design considerations for constrained targets.
const reading = 23;const message = "sensor: " + reading; console.log(message);Line 1 stores a sensor reading. Line 2 builds a short message. Line 4 prints it, so the output is sensor: 23.
A microcontroller often has a fixed memory budget for the whole product: your data, display, network buffers, native code, and JavaScript all share it. That changes good engineering choices. A feature that is harmless in a large desktop process may not fit on a device with a very small RAM limit.
The important claim is not that every XS program is tiny. It is that the host environment values predictable, resource-aware choices. Moddable's documentation includes precompiled modules and tools for linking JavaScript into embedded products, which fits that goal.
When comparing an embedded engine, ask which host APIs exist, how code is prepared, how memory is budgeted, and how the device is updated. Syntax support is only one part of the decision.
engine262 reads the spec closely
engine262 is an implementation of ECMA-262 written in JavaScript. Its project states three goals: specification compliance, introspection, and ease of modification. It also names speed at the expense of those goals as a non-goal.
const price = 3;const count = 2; console.log(price + count);Line 1 creates price. Line 2 creates count. Line 4 adds them, so it prints 5.
A spec interpreter is useful when the question is “what should JavaScript mean?” It can make a proposed rule easier to prototype and inspect than changing an optimizing production engine first. engine262 says it has also been used to find bugs in ECMA-262 and test262.
A teacher can follow a recipe word by word to check whether the recipe itself makes sense. It is slow on purpose. engine262 plays that role for language rules: it helps people check and explore the book, not serve a busy restaurant.
- In real life: A teacher follows the recipe word by word
- In JavaScript: engine262 follows specification operations closely
- In real life: The teacher checks the recipe
- In JavaScript: The engine helps inspect language rules
- In real life: The meal takes longer
- In JavaScript: Speed is not the target
- In real life: A changed recipe can be tried
- In JavaScript: A proposal can be explored
Where the analogy stops: A specification is more detailed than a recipe, and engine262 is still software with its own implementation choices.
Use a spec interpreter to understand, test, and discuss semantics. Use a production engine when your job is delivering an app under real performance and platform constraints.
A tiny bytecode interpreter
Bytecode is a compact instruction format that an engine can interpret. Production bytecode formats are private and much more complex. This model has only three operations: push a number, add two values, and print a value.
const program = [ { op: "push", value: 2 }, { op: "push", value: 3 }, { op: "add" }, { op: "print" },]; console.log(runBytecode(program).printed[0]);Lines 2 and 3 put 2 and 3 into the program. Line 4 asks for addition. Line 5 asks for printing. Line 8 runs the lesson's interpreter and logs the first printed value, so it prints 5.
Step through an instrumented replay of the lesson's tiny bytecode interpreter. It is a teaching model, not a view into a production engine.
script
{ op: "push", value: 2 }, { op: "push", value: 3 }, { op: "add" }, { op: "print" },]; console.log(runBytecode(program).printed[0]);The replay uses the same runBytecode function as the tests. It is an instrumented teaching model, not an engine debugger. Watch how the stack becomes 2, then 2 and 3, then 5, before print reads 5.
Small interpreters can benefit from such a compact representation without needing to turn every repeated operation into native machine code. The trade-off is usually less peak speed than a capable JIT after long warm-up.
Choose for the device
Engine selection starts with a constraint, not a brand name. The next model chooses among four deliberately simplified profiles. Its scores are made up and must never be used as benchmark evidence.
const device = "phone";const result = chooseEngine(device); console.log(result.profile.label);Line 1 names a phone. Line 2 asks the model for a fit. Line 4 logs the chosen label. In this model it would print Bytecode interpreter with AOT bytecode.
const device = "phone";const result = chooseEngine(device); console.log(result.profile.label);Phone appThe app should open quickly without carrying a large runtime cost.
Bytecode interpreter with AOT bytecodeBuild-time bytecode avoids parsing the app when it opens.
startup 4 / speed 10 / memory 8It can spend memory and warm-up work to make repeated code fast.
startup 9 / speed 6 / memory 5Build-time bytecode avoids parsing the app when it opens.
startup 8 / speed 4 / memory 9A small runtime leaves scarce device memory for the product.
startup 3 / speed 1 / memory 3It favors readable specification behavior over application speed.
Made-up model result: Phone app: Build-time bytecode avoids parsing the app when it opens.
Replay the actual lesson chooser. Its scores are deliberately made up to show competing startup, speed, and memory goals.
script
const result = chooseEngine(device); console.log(result.profile.label);Change exactly one input: the device. The reset button restores the phone app. Compare the reason first, then the made-up scores. A server asks for peak speed; spec testing asks for inspectable behavior; the microcontroller asks for memory discipline.
| Engine | Typical context | Documented focus | Practical reading |
|---|---|---|---|
| Hermes | React Native | Ahead-of-time static optimization and compact bytecode | Fast application startup is a stated focus. |
| QuickJS | Embedded programs | Bytecode interpreter with reference counting and cycle removal | Small and embeddable are its stated goals. |
| XS | Moddable embedded SDK | JavaScript engine for resource-constrained targets | The docs include precompiled modules and resource-constrained design choices. |
| engine262 | ECMA-262 research | JavaScript implementation of ECMA-262 | It explicitly puts compliance, introspection, and modification before speed. |
Practical selection questions
Most application teams do not swap engines casually. Their framework, host APIs, security model, debugger, native bindings, and deployment workflow may decide the choice before a microbenchmark does. The useful skill is knowing which question to ask.
- Is the important moment cold startup, a long-running hot path, or a tiny memory budget?
- Which JavaScript features and host APIs does the product actually need?
- How will the team measure startup, memory, and responsiveness on target hardware?
- Does the task need a production runtime, an embeddable scripting engine, or a specification experiment?
Measure the product on the device that users own. A server result does not prove a phone result. A desktop simulator does not prove a microcontroller result. A familiar engine name does not replace a memory budget.
Keep the decision reversible when you can. Write a small workload that resembles the product, record the startup path and memory use, then test it on the actual target. A clear constraint makes a comparison useful; a vague speed question produces a vague answer.
- A phone app opens from prepared bytecode.
- A React Native app uses Hermes.
- A device has only 256 KB RAM in this lesson model.
- A native tool needs an embeddable JavaScript engine.
- A language proposal needs a behavior playground.
- A team investigates an ECMA-262 or test262 question.
Sort each situation by the main constraint it highlights. The cards are prompts for reasoning, not universal engine recommendations.
Common misconceptions
- “Small means old JavaScript.” Small is a design constraint, not a language-era label.
- “AOT bytecode is universal native code.” It is prepared bytecode for an interpreter, not a promise about every CPU.
- “Reference counting cannot handle cycles.” Counting alone cannot; QuickJS documents cycle removal too.
- “A spec interpreter should win a speed test.” engine262 says speed at the expense of its stated goals is a non-goal.
- “The chooser scores are facts.” They are made-up teaching scores only.
| Term | What it means | Not this |
|---|---|---|
| Small engine | An engine designed around constrained goals | An engine that cannot run modern JavaScript. |
| AOT bytecode | Code prepared before the app is installed | Native machine code for every device. |
| Reference counting | Counted ownership with cycle handling in QuickJS | A promise that every engine frees memory the same way. |
| Spec interpreter | A tool for checking and exploring language behavior | A benchmark target for production app speed. |
Practice exercises
Predict the output before running the code.
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: four loop turns add 3 to the running total.
What does this little 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, removes both values, pushes their sum, and logs that sum.
Type the profile label the phone model should use.
const phoneChoice = "Bytecode interpreter with AOT bytecode";
console.log(phoneChoice);The model chooses Bytecode interpreter with AOT bytecode. It represents preparing work before a phone app opens.
Which memory technique does the QuickJS documentation name?
QuickJS documents reference counting with cycle removal. The cycle part matters because counts alone can retain a detached cycle.
A team is building a small device controller. What should it ask before choosing an engine?
Ask what constraint matters most: startup, memory, speed, or inspectable behavior. That narrows the right kind of engine before comparisons begin.
A language team wants to inspect a proposed JavaScript rule. Which engine in this lesson fits that job?
Use engine262 for the experiment. Its project focuses on ECMA-262 compliance, introspection, and ease of modification.
Check your understanding
Use the job, then the constraint. Keep the model scores separate from the official 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 statement matches QuickJS documentation?
Choose an answer to see the explanation.
Question 4 of 7What does this tiny bytecode model 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 scores, which target favors the small interpreter?
Choose an answer to see the explanation.
Question 7 of 7What is the useful first selection question?
Choose an answer to see the explanation.
Key takeaways
- Small engines choose a narrow goal such as startup, memory, embedding, or specification work.
- Hermes prepares compact bytecode with ahead-of-time static optimization for React Native startup.
- QuickJS is small and embeddable, uses bytecode, and documents reference counting with cycle removal.
- XS is part of the Moddable SDK for resource-constrained embedded targets.
- engine262 prioritizes specification compliance, introspection, and modification over speed.
- Choose from the product constraint, then measure on the target device.
Remember the one-liner.
A small engine is not universally slower or worse; it is built to spend its limited startup, memory, and complexity budget on a different job.
Coming next: JavaScript & WebAssembly, where JavaScript calls a compact, portable binary format.