Meet the engines
Explore V8, SpiderMonkey, JavaScriptCore, smaller engines, shared pipelines, hosts, embedders, and tools for engine work.
- 01Name the enginesSay where V8, SpiderMonkey, JavaScriptCore, Hermes, QuickJS, and other small engines run.
- 02Follow the stepsFollow source text through tokens, a tree, bytecode, the interpreter, feedback, fast code, deopt, and garbage collection.
- 03Tell engine and host apartDecide whether a feature comes from the language (ECMA-262), the host, or the embedder that sets up the engine.
What a JavaScript engine does
You met engines and hosts in Engines, runtimes & hosts. Then you met Node, Deno, Bun, and edge runtimes in Runtimes. This lesson starts a new stage, called Inside the engine. The stage looks at what happens inside the engine itself. This first lesson gives you the big picture. Later lessons look at each part up close.
A JavaScript engine is the program that runs JavaScript. It follows the rules in ECMA-262, the official JavaScript standard. Its main jobs are:
- Read your source text and split it into tokens: small pieces such as numbers, names, and
+. - Check the grammar and build a tree that shows how the code fits together.
- Turn that tree into simple instructions, and run them.
- Make code that runs often faster, and undo that safely when a guess turns out wrong.
- Free memory that nothing uses any more. This job is called garbage collection.
The host is the program around the engine, such as a browser or Node. The host decides what else your code can reach, such as the page, files, or timers.
Knowing the engine helps you read speed reports and strange error messages. But the car around the engine still matters. An error about the page usually comes from the browser. A note that fast code was thrown away comes from the engine. A failed build may come from neither.
- In real life: The engine makes the car move
- In JavaScript: The JavaScript engine turns your code into actions
- In real life: The car body adds seats, lights, and doors
- In JavaScript: The host adds the page, timers, files, and modules
- In real life: A mechanic can study the engine without studying the road
- In JavaScript: You can study parsing, bytecode, and memory without learning every browser feature
- In real life: One engine model can go into different cars
- In JavaScript: V8 is inside Chrome, Edge, Node.js, and Deno, and each one adds different extras
The last lesson was Finding memory leaks. There you used tools that look at a program from the outside. This lesson names the parts on the inside. The next lesson, Scanning source text, covers the very first step: turning characters into tokens.
V8, SpiderMonkey & JavaScriptCore
BIG THREEMost web developers meet three big engines:
- V8 is built by Google's V8 project. It runs in Chrome, Edge, Node.js, and Deno.
- SpiderMonkey is built by Mozilla. It runs in Firefox.
- JavaScriptCore is the engine of WebKit, the browser engine behind Safari. Bun uses it too.
Each engine runs code in tiers. A tier is one way of running code. A low tier starts quickly but runs slowly. A high tier takes longer to prepare but runs fast. The tier names in the table below were checked against each engine's own docs in September 2026. Engine teams rename and replace tiers often, so use the names as a rough map.
const prices = [4, 6];const total = prices.reduce((sum, price) => sum + price, 0);console.log(total);Line 1 makes an array of two prices, 4 and 6. Line 2 adds them up with reduce. The reduce method walks through the array and keeps a running total, starting at 0. Line 3 prints 10. Every engine must give this same result. Inside, each engine may use different instructions to get there. That is allowed, as long as the result your code sees is the same.
10.Read each row from left to right: the engine, who builds it, where you meet it, and how it runs code.
| Engine | Who builds it | Where you meet it | How it runs code (tiers, first to fastest) | Good to know |
|---|---|---|---|---|
| V8 | Google, as the open-source V8 project. | Chrome, Edge, and other Chromium browsers, Node.js, and Deno. | Ignition, Sparkplug, Maglev, TurboFan. | The tier names change often, so check the V8 docs for the latest ones. |
| SpiderMonkey | Mozilla. | Firefox. | Baseline Interpreter, Baseline Compiler, WarpMonkey. | WarpMonkey is the fastest tier. It builds its machine code with parts from an older tier called Ion. |
| JavaScriptCore | Apple, as part of WebKit. | Safari, apps that show web pages with WebKit, and Bun. | LLInt, Baseline JIT, DFG JIT, FTL JIT. | WebKit's own posts use these four tier names. |
| Hermes | Meta, for React Native. | React Native phone apps. | Bytecode made before the app ships, run by an interpreter that starts quickly. | When you build a React Native app for release, it turns your JavaScript into Hermes bytecode ahead of time. |
| QuickJS / QuickJS-ng | Fabrice Bellard and Charlie Gordon. QuickJS-ng is a copy kept up by the community. | Programs that build in a small engine, command-line tools, tests, and small devices. | One small interpreter that runs bytecode, with memory freed by reference counting. | Great to build into other programs. It is not meant to replace a browser engine. |
You don't need to remember the tiers. Keep this pattern instead. An engine first runs code in a tier that starts quickly. While the code runs, the engine takes notes about the values it sees. When some code runs many times, the engine spends extra time to make it faster. Later lessons explain each tier. They also explain why fast code must be able to fall back to a slower tier.
Hermes, QuickJS & embedded engines
The big three are not the only engines. Smaller engines make different choices. Some start up faster. Some use very little memory. Some fit inside a Java program. Some exist to test the language rules. They are useful when JavaScript runs inside something else: a phone app, a small device, a test tool, or a program written in another language.
Each row names an engine, where it usually runs, and what it is good at.
| Engine | Where it usually runs | What it is good at |
|---|---|---|
| Hermes | React Native phone apps, where it is the default engine. | Quick start-up and low memory. Release builds turn JavaScript into Hermes bytecode ahead of time. |
| QuickJS / QuickJS-ng | Other programs, as a tiny engine written in C. | A small interpreter that starts fast and supports most of the language. It frees memory by reference counting, plus a pass that clears loops of objects that point to each other. |
| Boa | Projects written in Rust. | A young engine built in Rust. Its team uses Test262 to check that it follows the language rules. |
| LibJS | The SerenityOS system and the Ladybird browser. | A JavaScript engine for a browser, first built as part of SerenityOS. |
| JerryScript | Small internet-connected devices. | Runs on tiny chips (microcontrollers) that have only tens of kilobytes of memory. |
| Moddable XS | Apps for microcontrollers. | Runs modern JavaScript for the Moddable tool kit, with very little memory and storage. |
| GraalJS | Java programs, through GraalVM. | Follows the language rules and works closely with Java code. It can also run on its own. |
const status = { ready: true };console.log(JSON.stringify(status));Line 1 makes a plain object. Line 2 uses JSON.stringify, which turns the object into text, and prints {"ready":true}. JSON is part of the language. So every engine provides it, even with no web page around. The same goes for objects, arrays, and promises. Picking a different engine changes speed, memory use, and tools. It must not change the language rules.
An embedded engine is one that is built into another program or device. Many embedded engines have no web page, and many skip the fastest tiers. They still follow the language rules carefully. They also care about quick start-up, steady memory use, and safe links to the program around them. An engine for a tiny chip and an engine for a browser solve different real problems.
The steps every engine shares
STEP THROUGHEngines differ inside, but they follow similar steps. Together the steps form a pipeline: the output of one step is the input of the next.
- The scanner reads the source text and splits it into tokens.
- The parser checks the grammar and builds a tree called an AST (abstract syntax tree). It may skip the inside of a function until that function is needed. This is called lazy parsing.
- The engine turns the tree into bytecode: simple instructions made for the engine, not for the computer's chip.
- An interpreter runs the bytecode, one instruction at a time.
- While the code runs, the engine keeps feedback: notes about the types and objects it sees.
- A JIT compiler ("just in time") turns code that runs often into fast machine code. It uses the feedback to guess what the code will see next.
- If a guess turns out wrong, the engine drops the fast code and goes back to a slower tier. This is called deoptimization, or "deopt" for short.
Beside all these steps, the garbage collector frees memory that your program can no longer reach.
function formatReceipt(total) { return "Total: " + total;}console.log("shop ready");Line 1 declares formatReceipt, but nothing calls it. Line 4 prints shop ready, and that is the only output. A function that never runs is called cold. The engine must still check the whole file for grammar mistakes. But it may wait to fully parse or compile a cold function until something calls it. Each engine picks its own way to do this, and your code can't tell the difference.
console.log("before");const price = ;console.log("after");Line 2 has nothing after =. That breaks the grammar, so the parser throws a SyntaxError before any line runs. So neither before nor after is printed. A runtime error is different. It happens while the code runs, so the lines before it have already run.
The walkthrough below runs these steps on a small sum, 2 + 3 * 4.
2 + 3 * 4. Each step turns the last one into a new form, until the result 14 comes out.Step through a tiny version of the engine's steps: source text, tokens, tree, bytecode, and the result.
script
const tokens = ["num:2","+/-:+","num:3","*:*","num:4"];const astRoot = "BinaryExpression";const bytecode = ["PUSH 2","PUSH 3","PUSH 4","MUL","ADD"];const result = 14;console.log(result);Line 2 holds the tokens from the scanner. Line 3 holds the type of the node at the top of the tree, from the parser. Line 4 holds teaching bytecode. It is made up for this lesson. It is not the bytecode of V8 (called Ignition), SpiderMonkey, or JavaScriptCore. It is tiny on purpose, so you can see the idea before later lessons show real engine bytecode.
See real tokens, a real tree, and toy bytecode
LIVEThis playground runs in your browser. It uses Acorn, a JavaScript parser written in JavaScript. Acorn gives the real tokens and a real tree. The tree follows ESTree, a shared format for JavaScript trees that many tools use. Then a small compiler from this lesson turns the tree into bytecode. It only accepts arithmetic, such as 2 + 3 * 4. That bytecode is a teaching model you can step through. It is not copied from any real engine.
const sourceText = "2 + 3 * 4";const tokens = ["num:2","+/-:+","num:3","*:*","num:4"];const astRoot = "BinaryExpression";const bytecode = ["PUSH 2","PUSH 3","PUSH 4","MUL","ADD"];const result = 14;console.log(result);Tokens
num2 0-1+/-+ 2-3num3 4-5** 6-7num4 8-9
Part of the tree (ESTree format)
{
"type": "BinaryExpression",
"start": 0,
"end": 9,
"operator": "+",
"left": {
"type": "Literal",
"start": 0,
"end": 1,
"value": 2,
"raw": "2"
},
"right": {
"type": "BinaryExpression",
"start": 4,
"end": 9,
"operator": "*",
"left": {
"type": "Literal",
"start": 4,
"end": 5,
"value": 3,
"raw": "3"
},
"right": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 4,
"raw": "4"
}
}
}Toy bytecode and the stack after each step
PUSH 2stack [2]PUSH 3stack [2, 3]PUSH 4stack [2, 3, 4]MULstack [2, 12]ADDstack [14]
Acorn made 5 tokens. The top node of the tree is BinaryExpression. The teaching bytecode gives 14.
Try (7 - 1) / 3 and 5 * (2 + 6), and watch the tree change shape. Brackets and the order of operations (* before +) decide the shape of the tree before any bytecode is made. That is why the scanner and parser lessons come before the bytecode and JIT lessons in this stage.
Making code fast, and freeing memory
OPTIMIZEAn interpreter starts running code quickly, but it runs slowly. A JIT compiler needs time to build machine code, but that code runs fast. So engines only use the JIT for hot code: code that runs many times.
The JIT makes a guess from the feedback. For example: "this function has only seen numbers, so it will keep getting numbers." The fast code checks the guess each time it runs. This check is called a guard. If a later call breaks the guess, the guard fails. The engine then deoptimizes: it drops the fast code and goes back to a slower tier that handles every case correctly.
function userLabel(user) { return user.name + ": " + user.score;}console.log(userLabel({ name: "Asha", score: 3 }));console.log(userLabel({ name: "Mina", score: 4 }));Lines 4 and 5 call userLabel with two objects. Both objects have the same keys, name and score, made in the same order. An object's shape is its list of keys in that order. Line 4 prints Asha: 3, and line 5 prints Mina: 4. Seeing the same shape again and again gives the engine useful feedback, so it may make this function faster. That is not a promise. Don't make your data harder to read just to keep shapes the same.
The walkthrough below is a small model of feedback and deopt. A warm-up call teaches the engine to expect numbers. Then a later call either sends numbers again or sends a string.
add(1, 2) teaches the engine to expect numbers. A later call with numbers stays in the fast code. A call with a string fails the check and goes back to a lower tier.Step through how feedback lets an engine build fast code from a guess, and why a later change of type can make it deopt.
script
return a + b;} const feedback = [];feedback.push(typeof 1 + "+" + typeof 2);const first = add(1, 2);const optimizedFor = feedback[0];const later = add("3", 4);const outcome = typeof later === "number" ? "stay in optimized code" : "deopt to a lower tier";console.log(first);console.log(outcome);Garbage collection is not a last step after the code finishes. It runs beside the pipeline the whole time. Many things use memory: your values, objects, functions, and closures (functions that remember the variables around them). The engine's own bytecode, feedback, and fast code use memory too, and so do objects from the host. That is why memory lessons and engine lessons meet again when you read heap snapshots or look for leaks.
Learn these steps so you can find problems and measure better. Don't use them to rewrite small bits of code by guessing. Engines change often. A small rewrite can help one tier, hurt another, or make no difference next to the network, drawing the page, or a slow algorithm.
Engine vs host vs embedder
BOUNDARIESThree parts work together when JavaScript runs:
- The language. ECMA-262 defines the grammar, the values, the objects, how code runs, and built-in tools such as
PromiseandArray.prototype.map. Engines build all of this. - The host. It connects JavaScript to the outside world. In a browser, the HTML standard defines the event loop, timers, the page (called the DOM), and how modules load. Node defines
process, file modules, how packages are found, and its own rules. - The embedder. This is the program that starts the engine, usually written in C++, Rust, or Java. It calls the engine's own functions to make an isolate (one copy of the engine with its own memory) and a context (one global world for code to run in). It also picks which outside objects JavaScript can use.
In a browser or in Node, one program is both the host and the embedder. The two words name two jobs: adding the extras, and setting up the engine.
Promise to every engine. A host adds names such as document. The embedder sets up the engine before your code runs.Each row lists some names, who provides them, and how to think about them.
| Names | Who provides them | What this means |
|---|---|---|
let, functions, objects, Array.prototype.map, Promise, JSON, globalThis | The language, ECMA-262. | Every engine builds these, so they work everywhere. |
The page (DOM), window, document, drawing the page, clicks and key presses, much of how modules load | Browser standards such as HTML and DOM. | The browser puts an engine inside and adds tools for the page. |
Timers, the event loop, fetch, streams, modules, workers | Host standards and each runtime's own choices. | Browsers and server runtimes often have the same names, but different rules and limits. |
process, node:fs, node:path, how packages are found | The Node.js host. | These are not part of the JavaScript language. |
| Isolates, contexts, links to C++ code, saved start-up states (snapshots), command-line flags | The embedder, through the engine's own functions. | The program that starts the engine decides how to set it up and which outside objects to hand in. |
PromiseArray.prototype.mapdocument.querySelectorrequestAnimationFramenode:fs/promisesprocess.versions.v8- make a V8 isolate
- hand a C++ object to JS
Sort each card by who provides it. Then read the explanation on each card. Some cards are close calls on purpose.
The short check below uses only names from the language. So it runs both in the browser editor and in Node. It uses nothing from the page and nothing that only Node has.
console.log(typeof Array.prototype.map);console.log(typeof globalThis);console.log(typeof Promise);typeof gives the kind of a value, as text. Each line asks for the kind of one built-in name: Array.prototype.map, globalThis (the global object), and Promise.
const hasPageApis = typeof document !== "undefined";console.log(hasPageApis ? "page APIs available" : "no page APIs");Line 1 uses typeof to check whether this host has a document. A plain read of a missing name throws an error, but typeof does not. In the lesson editor, line 2 prints page APIs available. In plain Node, it prints no page APIs. Checking for the feature itself is safer than checking the engine's name. V8 runs in both Chrome and Node, so the engine name can't tell you if there is a page.
Embedder code looks different, because it is not normal page code. It is the program around the engine. It makes a copy of the engine and a context, adds outside objects, and only then runs the user's code.
// Pseudocode: embedders call engine APIs from C++/Rust/Java.const isolate = createIsolate();const context = isolate.createContext({ console });context.evaluate("console.log(1 + 2)");Line 1 is a comment: real embedder code is written in C++, Rust, or Java. Line 2 makes an isolate. Line 3 makes a context and hands it console. Line 4 runs one line of JavaScript inside that context.
Exploring engines with d8, jsvu & eshost
TOOLSEngine tools are useful, but each one depends on the version you have installed. Here is what each tool does:
d8is V8's own shell: a small program that runs JavaScript on V8, with no browser around it.jsvudownloads recent engine shells, so you don't have to build them yourself.eshost-cliruns the same code in several engines, so you can compare the results.node --v8-optionslists the V8 settings, called flags, that your Node build lets you turn on.- Test262 is the official test suite for the language. Engine teams run it to check that they follow ECMA-262.
$ node --version$ node -p "process.versions.v8"$ node --v8-options | grep -- --print-bytecodeLine 1 prints the Node version. Line 2 prints the version of V8 inside this Node. Line 3 lists all V8 flags, and grep keeps only the line for --print-bytecode. That flag makes V8 print the bytecode it builds.
To find the V8 version in Chrome, open chrome://version in Chrome and read the JavaScript line. Chrome, Node, Deno, and a d8 shell can each have a different V8 version, so don't assume they match.
# V8's developer shell after building V8 or installing a binary with jsvuv8 --print-bytecode demo.js # Install current engine shells without compiling them yourselfnpx jsvu --engines=v8,spidermonkey,javascriptcore,quickjs,hermes,xs # Register shells and compare behavior across enginesnpx eshost-cli --configure-jsvueshost -e 'print(1 + 2)' # Run the official ECMAScript conformance suite when building an enginenpm test -- path/to/test262You can't run these commands on this page. The editor here is not a terminal, and most engine shells are not installed. The lesson's tests do run the Node commands above. They check that Node 22 has V8 12.4 inside and offers --print-bytecode. The last part of a version number changes from one install to the next, so use the values your own computer prints.
Where engine knowledge helps
You rarely need to write code for one tier. But knowing the steps tells you where to look when something is slow or broken. Start with the simplest win: a good way to store your data.
const pricesByItem = new Map([["tea", 3], ["cake", 5]]);console.log(pricesByItem.get("tea"));Line 1 makes a Map: a list of keys, each with a value. Here each key is an item name and each value is a price. Line 2 asks for the price of tea directly and prints 3. As the list of items grows, a good data structure like this usually helps more than changing small bits of code to please one JIT. Measure the real program, including loading, parsing, network, drawing the page, and memory use.
Here is where engine knowledge helps in real work:
- A page starts slowly. Ask how much code must be downloaded, scanned, parsed, compiled, and run before the user can click.
- A profile shows one slow function. Measure first. Then look for a better algorithm, or for fewer new objects.
- A heap snapshot shows objects that should be gone. Follow the links from closures and host objects back to the roots. The roots are the starting points the garbage collector always keeps, such as global variables.
- Code moves between the browser, Node, Deno, Bun, or a small device. Separate the language rules from the host's extras.
Keep Runtime overview, Manuals & specs, and The event loop close. They explain the host side, which the engine does not own.
Common misconceptions
- “V8 is the JavaScript standard.” V8 is one engine that follows the standard. The standard is ECMA-262.
- “A JIT always makes code faster.” Building fast code takes time and memory. Code that runs only a few times may never leave the first tiers.
- “Bytecode is the same in every engine.” Each engine has its own bytecode and its own tiers. This lesson's bytecode is only a teaching model.
- “The engine owns timers and the page.” The host adds timers, page drawing, files, network, and most global objects around the engine.
- “Garbage collection fixes leaks by itself.” The garbage collector only frees objects that nothing can reach. If your code or a host object still points to some data, that data stays.
- “Engine flags are safe to use in real apps.” Flags are tools for studying engines. They can change, disappear, or act differently in the next version.
These four words are easy to mix up. Each row says what the word means and what it does not mean.
| Word | What it means | What it does not mean |
|---|---|---|
| Engine | The program that follows the language rules, runs the code, and manages its memory. | A full browser or a full server program. |
| Host | The program around the engine. It adds outside-world tools, such as the page or files. | The parser or the JIT on its own. |
| Runtime | A complete program you run code in, such as Node, Deno, Bun, a browser tab, or an edge worker. | Only the language rules. |
| Embedder | The program code that starts the engine, makes isolates and contexts, and hands objects to JavaScript. | Normal JavaScript app code. |
Practice exercises
5 EXERCISESUse the live playground. What kind of token comes first in 2 + 3 * 4?
const tokenValues = ["num:2", "+", "num:3", "*", "num:4"];
console.log(tokenValues.join(" | "));The first token is the number 2. Acorn labels it num.
Look at the toy compiler's bytecode for 2 + 3 * 4. Type the last instruction.
const bytecode = ["PUSH 2", "PUSH 3", "PUSH 4", "MUL", "ADD"];
console.log(bytecode.at(-1));The last instruction is ADD, because + is at the top of the tree.
Who provides document.querySelector: the engine, the browser host, Node, or Test262?
document.querySelector comes from the browser host. It is one of the page tools (the DOM), so the engine alone does not have it.
Which Node property does this lesson use to show the version of V8 inside Node?
In Node, process.versions.v8 holds the version of the V8 engine inside it.
Fast code was built for adding numbers. Why might the engine drop it after a later call with "1"?
function add(a, b) {
return a + b;
}
console.log(add(1, 2));
console.log(add("1", 2));The fast code guessed that + adds two numbers. A later call with a string breaks that guess, because + now joins text. The guard fails, so the engine goes back to a slower tier. This is a deopt.
Quiz: engines, steps, and who provides what
8 QUESTIONSThink about the pipeline steps and about who provides each feature. Code answers are real JavaScript output.
Question 1 of 8Which sentence best describes a JavaScript engine?
Choose an answer to see the explanation.
Question 2 of 8Which engine and list of tiers matches Chrome and Node, as of this lesson's sources?
Choose an answer to see the explanation.
Question 3 of 8What result does the toy pipeline give for
2 + 3 * 4?Choose an answer to see the explanation.
Question 4 of 8What does this code print?
Read the code, then predictfunction add(a, b) { return a + b; } console.log(add(1, 2)); console.log(add("1", 2));Choose an answer to see the explanation.
Question 5 of 8Which tool comes from the browser host, not from ECMA-262?
Choose an answer to see the explanation.
Question 6 of 8What does this code print, both in browsers and in Node?
Read the code, then predictconsole.log(typeof Array.prototype.map); console.log(typeof globalThis); console.log(typeof Promise);Choose an answer to see the explanation.
Question 7 of 8What is
d8useful for?Choose an answer to see the explanation.
Question 8 of 8Why shouldn't you rewrite small bits of code just to please an engine?
Choose an answer to see the explanation.
Key takeaways and what comes next
- V8, SpiderMonkey, and JavaScriptCore are the big browser engines. Hermes, QuickJS, Boa, LibJS, JerryScript, XS, and GraalJS serve other needs, such as phone apps and small devices.
- The shared steps are: source text, tokens, tree (AST), bytecode, interpreter, feedback, JIT tiers, and sometimes deopt. The garbage collector runs beside all of them.
- ECMA-262 defines the language. Hosts add the page, timers, files, modules, the event loop, and globals. Embedders start the engine and hand it outside objects.
- d8, jsvu, eshost-cli, Node's V8 flags, and Test262 are tools for studying and testing engines. Don't depend on them in a real app.
- Engine knowledge helps you find slow start-up, memory, and speed problems. Measuring still matters more than tips you heard somewhere.
In one line: the engine runs the language, the host adds the world around it, and measuring beats guessing.
The next lessons in this stage, and what each one explains:
| Next lesson | What it explains |
|---|---|
| Scanning source text | How characters become tokens, including /, which can mean two things, and names written with escapes. |
| Parsing and ASTs | Grammar rules, errors found before code runs, the ESTree format, and why a parser sometimes reads code twice. |
| Lazy parsing and startup | Why engines wait before preparing functions that might never run. |
| Scope analysis and bytecode | How the engine finds each variable, handles closures, and builds bytecode before running it. |
| Tiered compilation, feedback, inline caches, GC | How engines make hot code fast, drop it safely, and free memory. |
Further reading from the engine teams
- V8 Ignition docs, Sparkplug, Maglev, and TurboFan.
- Mozilla SpiderMonkey source docs, with overviews of the parser, bytecode, tiers, bailouts, and the garbage collector.
- WebKit's FTL JIT overview and the JavaScriptCore post on guessing (speculation).
- React Native Hermes docs, QuickJS, QuickJS-ng, JerryScript, and Moddable XS.
- V8 d8 docs, jsvu, eshost-cli, and Test262.
Up next: Scanning source text, where the first stream of characters becomes tokens.