69. JavaScriptCore and SpiderMonkey
Every benchmark in this book ran on V8. Safari and Firefox run the same JavaScript on different machinery: other tiers, other thresholds, even another way of putting a number in 64 bits. Here is what their own source code says.
- Name the execution tiers of JavaScriptCore and SpiderMonkey, as their own sources name them.
- Say where each engine resumes when an optimized guess fails.
- Encode an integer and a double the way JavaScriptCore does, and compare it with V8 and SpiderMonkey.
- Explain why a speed-up measured on one engine is only a hypothesis on another.
One loop, three engines
Every number this book has measured came from Node, which runs V8. Open the same page in Safari and JavaScriptCore runs it. In Firefox, SpiderMonkey does. All three follow one specification[13], so your program prints the same thing everywhere. Nothing in that specification says how fast anything must be.
So is struct-of-arrays faster in Safari too? This book never measured it, and this chapter won’t pretend. What it can do is show you, from the engines’ own source code, how differently they are built. Then “it was 4× faster on my machine” stops sounding like a law of nature.
Same ladder, different rungs
Three restaurants cook from the same recipe book. Each starts a new dish slowly, by the book, then lets a faster cook take over once it is ordered often. But one has two fast cooks, another has two careful ones, and they promote a dish after a different number of orders.
Chapter 61 climbed V8’s ladder: Ignition, then Sparkplug, Maglev and TurboFan[1]. Step through all three engines:
JavaScriptCore has four tiers. Its option list names them: the
LLInt, the Baseline JIT, the DFG JIT and the FTL
JIT[3]. The LLInt (“low-level interpreter”)
isn’t written in C++. It is written in an assembly-like language of
JavaScriptCore’s own, with macros and per-platform ifs, which is
translated for each processor[4]. The same option
list shows that even this interpreter has inline caches for property
accesses and calls. At the top, the FTL takes the DFG’s view of a
function and lowers it into a second intermediate representation, the
compiler’s private form of the program (Part VI is all about them),
called B3, for heavier optimization[6].
SpiderMonkey has a C++ interpreter, a Baseline Interpreter, a Baseline JIT and an optimizing compiler, Ion. Its options file states the default warm-up counts:
// js/src/jit/JitOptions.cpp, trimmed
// "invocations or loop iterations"
SET_DEFAULT(
baselineInterpreterWarmUpThreshold, 10);
SET_DEFAULT(
baselineJitWarmUpThreshold, 100);
SET_DEFAULT(
normalIonWarmUpThreshold, 1500);
Each count is “invocations or loop iterations”[8]. The front end of Ion is called Warp. Its oracle takes a snapshot of what the Baseline inline caches saw, and WarpBuilder turns the bytecode into Ion’s own representation off the main thread[9]. Those caches are written in CacheIR, “an (extremely simple) linear IR language for inline caches” (IR: intermediate representation), from which both Baseline and Ion produce machine code[10].
Where a failed guess lands
All three speculate (chapter 66), and all three can be wrong. What differs is where they go back to. V8’s deoptimizer rebuilds interpreter frames[1]. JavaScriptCore’s DFG code describes “how to exit the speculative path by going into baseline code”[5]. In SpiderMonkey, a bailout recovers “a baseline interpreter frame” from Ion’s frame, for example when a shape guard fails or an integer add overflows. Values may be unboxed or optimized away, and inlined frames may be missing, so small snapshots record how to rebuild each one[11].
Same number, three encodings
Tiers aren’t the only difference. Even a single value is stored differently. Chapter 22 showed V8’s choice: a small integer is a Smi, and a double usually lives in a separate HeapNumber. Chapter 60 showed SpiderMonkey’s: every value is NaN-boxed, with a tag in the high bits of a double[12].
JavaScriptCore also uses NaN space, but the other way round. A pointer
is stored as it is, so its top 16 bits are zero. A 32-bit integer gets
the tag 0xFFFE on top. A double is stored as its own bits plus
249, which moves every real double out of both of those
ranges. Decoding subtracts it again[7]. Adding
249 to a 64-bit pattern adds 0x20000 to its high 32-bit
word, so a typed-array view (chapter 16) can show it:
const f64 = new Float64Array(1);
const u32 = new Uint32Array(f64.buffer);
const hex = (n) =>
n.toString(16).padStart(8, '0');
// int32: tag 0xFFFE on top
function jscInt(i) {
return 'fffe0000:' + hex(i >>> 0);
}
// double: its bits plus 2^49, which is
// 0x20000 in the high 32-bit word
function jscDouble(d) {
f64[0] = d;
const raw = u32[1];
const hi = (raw + 0x20000) >>> 0;
return hex(raw) + ' -> ' +
hex(hi) + ':' + hex(u32[0]);
}
console.log('5 ', jscInt(5));
console.log('-1 ', jscInt(-1));
console.log('1.5 ', jscDouble(1.5));
console.log('-2.5', jscDouble(-2.5));
console.log('NaN ', jscDouble(NaN));Engine diversity is the fact that JavaScript engines implementing the same specification make different implementation choices: how many tiers, when code moves between them, what each tier speculates on, where a failed guess resumes, and how values and objects are represented. So the same program can have different performance profiles in different engines, and a speed-up measured in one engine is a hypothesis for the others until it is measured there.
Viscosity’s output stores numeric fields in Float64Array columns. A
typed array’s elements are raw IEEE 754 values in an ArrayBuffer, in
every engine[14], so that part of the layout
doesn’t depend on whether an engine uses Smis, NaN-boxing or
JavaScriptCore’s encoding. How fast each engine’s optimizing tier
turns xs[i] += vs[i] into machine code is a different question.
The honest position: this book’s speed-ups are V8 numbers. If you care
about Safari or Firefox, measure there, with the same care as chapter
24.
Your turn
Write encode(v) for a number v, returning [hi, lo], the two 32-bit halves of its JavaScriptCore encoding. If v is a 32-bit integer (and not -0), hi is INT_TAG and lo is v >>> 0. Otherwise it is a double: take its bits through f64/u32 (u32[1] is the high word) and add OFFSET to the high word (wrapping with >>> 0). Write decode(hi, lo) to get the number back.
const f64 = new Float64Array(1);
const u32 = new Uint32Array(f64.buffer);
const INT_TAG = 0xfffe0000;
// 2^49, as seen in the high word:
const OFFSET = 0x20000;
function encode(v) {
return [0, 0];
}
function decode(hi, lo) {
return 0;
}A ladder is a list of [name, count] pairs sorted by count, such as SpiderMonkey’s defaults below. Write tierAt(n, ladder): the name of the highest tier whose count is at most n, the number of calls or loop iterations so far.
const SM = [
['interpreter', 0],
['baseline interpreter', 10],
['baseline jit', 100],
['ion', 1500],
];
function tierAt(n, ladder) {
return ladder[0][0];
}What you learned
- JavaScriptCore runs code in the LLInt, the Baseline JIT, the DFG JIT and the FTL JIT. The LLInt is written in its own portable assembly language. The FTL lowers the DFG’s view of a function into B3.
- SpiderMonkey has a C++ interpreter, the Baseline Interpreter, the Baseline JIT and Ion, whose front end Warp compiles from snapshots of baseline CacheIR caches.
- A failed guess resumes in V8’s interpreter, JSC’s baseline code, or SpiderMonkey’s Baseline Interpreter.
- JSC NaN-encodes values with raw pointers,
0xFFFE-tagged int32s and doubles offset by 249. - Engine diversity: one spec, many designs. A speed-up measured in one engine is a hypothesis in another.
Check your understanding
0/6- 1.
Which list gives JavaScriptCore’s tiers, as its own options name them?
- 2.
What does SpiderMonkey’s Warp use, besides the bytecode, to build optimized code?
- 3.
A shape guard fails in SpiderMonkey’s Ion code. What happens?
- 4.
In JavaScriptCore’s value encoding, what are the top 16 bits of the integer 5?
- 5.
Why might the same loop be faster in one engine than another?
- 6.
Viscosity measured its speed-ups on Node. What is the honest claim for Firefox?
References
- [1] Toon Verwaest, Leszek Swirski, Victor Gomes, Olivier Flückiger, Darius Mercadier, Camillo Bruni (2023). Maglev - V8’s Fastest Optimizing JIT. V8 blog (v8.dev), 5 December 2023. v8.dev/blog/maglev (checked 2026-10-31)
- [2] Leszek Swirski (2021). Sparkplug — a non-optimizing JavaScript compiler. V8 blog (v8.dev), 27 May 2021. v8.dev/blog/sparkplug (checked 2026-10-31)
- [3] WebKit contributors (2026). Source/JavaScriptCore/runtime/OptionsList.h: JavaScriptCore’s run-time options. WebKit source repository (github.com/WebKit/WebKit), main branch. github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/OptionsList.h (checked 2026-10-31)
- [4] WebKit contributors (2026). Source/JavaScriptCore/llint/LowLevelInterpreter.asm: the LLInt, written in offlineasm. WebKit source repository (github.com/WebKit/WebKit), main branch. github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/llint/LowLevelInterpreter.asm (checked 2026-10-31)
- [5] WebKit contributors (2026). Source/JavaScriptCore/dfg/DFGOSRExit.h: OSR exits from DFG code. WebKit source repository (github.com/WebKit/WebKit), main branch. github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/dfg/DFGOSRExit.h (checked 2026-10-31)
- [6] WebKit contributors (2026). Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp: lowering DFG IR to B3 for the FTL tier. WebKit source repository (github.com/WebKit/WebKit), main branch. github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp (checked 2026-10-31)
- [7] WebKit contributors (2026). Source/JavaScriptCore/runtime/JSCJSValue.h: the encoding of JSValue. WebKit source repository (github.com/WebKit/WebKit), main branch. github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/JSCJSValue.h (checked 2026-10-31)
- [8] Mozilla contributors (2026). js/src/jit/JitOptions.cpp: SpiderMonkey’s JIT options and warm-up thresholds. Firefox source repository (github.com/mozilla-firefox/firefox), main branch. github.com/mozilla-firefox/firefox/blob/main/js/src/jit/JitOptions.cpp (checked 2026-10-31)
- [9] Mozilla contributors (2026). js/src/jit/WarpOracle.h, WarpSnapshot.h and WarpBuilder.h: Warp, the front end of SpiderMonkey’s Ion compiler. Firefox source repository (github.com/mozilla-firefox/firefox), main branch. github.com/mozilla-firefox/firefox/blob/main/js/src/jit/WarpBuilder.h (checked 2026-10-31)
- [10] Mozilla contributors (2026). js/src/jit/CacheIR.h: [SMDOC] CacheIR. Firefox source repository (github.com/mozilla-firefox/firefox), main branch. github.com/mozilla-firefox/firefox/blob/main/js/src/jit/CacheIR.h (checked 2026-10-31)
- [11] Mozilla contributors (2026). js/src/jit/Bailouts.h: [SMDOC] IonMonkey Bailouts. Firefox source repository (github.com/mozilla-firefox/firefox), main branch. github.com/mozilla-firefox/firefox/blob/main/js/src/jit/Bailouts.h (checked 2026-10-31)
- [12] Mozilla contributors (2026). js/public/Value.h: JS::Value, SpiderMonkey’s representation of JavaScript values (Firefox source code). Firefox source repository (github.com/mozilla-firefox/firefox), Mozilla. github.com/mozilla-firefox/firefox/blob/main/js/public/Value.h (checked 2026-10-31)
- [13] Ecma International, Technical Committee 39 (TC39). ECMAScript® Language Specification (ECMA-262), current draft. Ecma International; living draft at tc39.es/ecma262, source at github.com/tc39/ecma262. tc39.es/ecma262/ (checked 2026-10-31)
- [14] MDN contributors. JavaScript typed arrays. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays (checked 2026-10-31)