Meta shipped a JavaScript engine with no just-in-time compiler in it, and wrote down why. That is the right place to start on JavaScript JIT compilation, because it forces the question the triumphant version of this story never asks: if speculative compilation is simply faster, why would anyone building a shipping engine deliberately decline it?

Because a JIT is not free speed. It is a purchase, paid in memory, warm-up and predictability, and what it buys is an answer to a genuinely hard problem: how do you optimise code when you cannot know a single type until it runs? You guess. You compile for the guess. And you keep a way back for when the guess is wrong.
Tiering, inline caches, hidden classes, deoptimisation and on-stack replacement all serve that sentence — and the same bet runs underneath you in silicon and in your database.
An engine that declines the bet
Hermes is Meta’s JavaScript engine for React Native, and its launch post of 12 July 2019 is unusually direct:
Hermes today has no JIT compiler. This means that Hermes underperforms some benchmarks, especially those that depend on CPU performance. This was an intentional choice: These benchmarks are generally not representative of mobile application workloads.
The reason is a metric. The post names three primary measures, the first being “The time it takes for the app to become usable, called time to interact (TTI)”, then download size and memory. Against those three a JIT is not neutral:
Because JITs must warm up when an application starts, they have trouble improving TTI and may even hurt TTI. Also, a JIT adds to native code size and memory consumption, which negatively affects our primary metrics.
That is the bill, itemised by a vendor: warm-up, code size, memory. Hermes moves compilation out of the runtime entirely — bytecode is produced ahead of time, in the app build — so the phone never pays a compile cost, and never receives a speculative speedup.
The position has held. Announcing the experimental opt-in to Hermes V1 on 8 October 2025, the React Native team wrote:
Hermes V1 does not yet contain JS-to-native compilation (previously known as “Static Hermes”) or the JIT compilation that was presented during React Native EU 2023.
Hermes V1 became the default engine in React Native 0.84 on 11 February 2026, and that release post does not mention a JIT at all; as of 9 September 2026 the engine’s repository still advertises “ahead-of-time static optimization and compact bytecode”. Short-lived, startup-sensitive, memory-constrained work is where a JIT fits worst.
Why one compiler was never going to be enough
The naive design — compile everything once with your best optimising compiler — fails on arithmetic. Most functions execute a handful of times, a small minority execute enormously often, and you cannot tell which is which before the program starts. So engines tier: start in the cheapest mode, promote a function only once it has earned the next tier’s compile budget. V8’s account of why it kept adding tiers rather than tuning one compiler states the ceiling exactly, from the Sparkplug launch post, 27 May 2021:
With our current two-compiler model, we can’t tier up to optimised code much faster; we can (and are) working on making the optimisation faster, but at some point you can only get faster by removing optimisation passes, which reduces peak performance.
One compiler has one dial. Turning it toward produces code quickly turns it away from produces good code. You cannot have both for the same function, so you build several compilers and route work between them.

Compile cost is only half the reason, though. The other half belongs to speculation specifically, and the same post continues:
Even worse, we can’t really start optimising earlier, because we won’t have stable object shape feedback yet.
Even if optimising compilation were instant, the engine could not do it immediately, because it has nothing to optimise for yet. The lower tiers are not merely cheap; they are the observation period — how the engine learns what to bet on.
The exact ladder inside any given engine is another article’s subject, and the components move. One dated figure shows the range they span. Maglev, V8’s mid-tier optimising compiler (5 December 2023), reports its position purely by compile speed:
In terms of compilation speed we’ve managed to build a JIT that’s roughly 10x slower than Sparkplug, and 10x faster than TurboFan.
Two orders of magnitude of compile budget separate the cheapest compiler in that engine from the most aggressive. That range is why tiering exists, and it also sets what a wrong guess costs: the post notes a cheaper optimising tier can deploy earlier precisely because “there’s no huge cost to deoptimizing and recompiling later.” The price of being wrong scales with what you spent being confident.
Inline caches: a note stuck to one line of code
Property access in a dynamically typed language is expensive in the general case. Does x live on the object, or up a prototype chain? Answering honestly on every access is the overhead that made early JavaScript slow.
An inline cache replaces the answer with a bet. One site in the code usually sees the same kind of object repeatedly, so the engine attaches a small cache there: last time this was shape S, and the property sat at offset 3. Next time it makes one cheap comparison — still shape S? — and jumps straight there. It has stopped proving and started asserting-and-checking.
That cache has three states, and they are the difference between a fast line of code and a slow one that looks identical.
| Cache state | What that one site has seen | On the next hit | What it costs you |
|---|---|---|---|
| Monomorphic | Exactly one shape, ever | One comparison, then a cached offset or target | Nothing — this is the fast path |
| Polymorphic | A small number of distinct shapes | Check candidates in sequence until one matches | Slower than one comparison, far better than a full lookup |
| Megamorphic | Too many shapes to track individually | Give up per-site caching; fall back to a shared table | Per-site specialisation is gone at that location |
The vocabulary is not a property-access curiosity even inside V8. Its post on speculative optimisations for WebAssembly, 24 June 2025, reaches for the same words about call targets — “indirect calls can target many different functions, but in practice, they still often go to a single target”, monomorphic or polymorphic — then records those targets and inlines “up to four target functions when generating the optimized code.”
⚠️ A caution about the number everybody quotes. The claim that four distinct shapes tips a V8 property-load cache into megamorphic comes from Vyacheslav Egorov’s explanation — a former V8 engineer, but a personal blog, dated 11 January 2015. His wording is “maximum possible capacity for the inline cache (e.g. 4 for property loads in V8)”, and on the state itself: “Megamorphic state exists to prevent uncontrolled growth of polymorphic caches”, after which V8 caches into “a global hashtable” instead of at the site. Excellent on the mechanism; an eleven-year-old figure for a tunable constant. It is not an official current number, and the “four” in the 2025 WebAssembly post above limits inlining targets in a different subsystem — do not merge them into one fact.

For code you actually write: a helper called with consistently shaped objects stays monomorphic and fast. The same helper reused across three or four kinds of record — a generic formatter, a handler taking different payloads — degrades at that site, and the source looks identical either way.
What the cache is actually comparing
None of that works unless “shape” is checkable with a single pointer comparison, which is what hidden classes are for. V8 calls them HiddenClasses; elsewhere they are “shapes” or “structures”. From Fast properties in V8, 30 August 2017:
The HiddenClass stores information about the shape of an object, and among other things, a mapping from property names to indices into the properties.
They are built incrementally, as a transition tree: from an empty object, adding "a" moves you to one child, adding "b" to a grandchild. The consequence is why a shape can be one comparison:
This transition tree makes sure you end up with the same final HiddenClass if you add the same properties in the same order.

Which says exactly what breaks it:
- The same properties in a different order walk a different path through the tree. Two objects with identical key sets — indistinguishable when logged — are structurally different to the engine.
- Properties added after construction, inconsistently across instances, fork the tree repeatedly, so objects a human calls one kind of thing become several shapes.
- Deleting properties, or accumulating enough ad-hoc keys, can demote an object to a dictionary representation, and V8 is blunt: “Since inline caches don’t work with dictionary properties”, such objects are slower. That object has opted out of the story entirely.
The link is explicit: “HiddenClasses serve as an identifier for the shape of an object and as such a very important ingredient for V8’s optimizing compiler and inline caches.” A hidden class is not metadata about your object. It is what every inline cache compares.
The bailout, and the loop underneath it
Every bet so far is provisional; nothing in a dynamically typed language guarantees the future resembles the past. Deoptimisation is what happens when the probability loses. Optimised machine code discovers mid-execution that an assumption no longer holds, stops, and hands control to a tier that does not make it — resuming at the point reached, with the right values in the right places.
That is not a failure; it is the safety valve that makes the scheme sound. It is not free either. To bail out correctly, the compiler must emit, alongside the fast code, metadata describing how to reconstruct unoptimised state wherever a bailout could occur. You pay on every optimised compile to build a map out of a place you hope never to visit.
The pathological case is an engine that learns nothing from the bailout: it reoptimises on the same failed assumption, deoptimises again, and cycles. V8’s retrospective Land ahoy: leaving the Sea of Nodes, 25 March 2025, describes exactly this in its own history with Crankshaft — “It suffered from many performance cliffs and bailouts”, where an edge case “could cause performance to drop by a factor 100”, and then:
Crankshaft would optimize a function using some speculative assumptions, then the function would get deoptimized when those assumptions didn’t hold, but too often, Crankshaft would reoptimize the function with the same assumptions, leading to endless optimization-deoptimization loops.

Note the severity of the response: not a tuned heuristic, a replaced compiler. The signature is a function that gets worse the more it is called — the opposite of what the engine has more data now predicts. The full anatomy of that cliff deserves separate treatment; the point here is narrower. The undo is the load-bearing half of the design, and a system that undoes without learning is worse than one that never guessed.
Optimising a loop that has not returned yet
One shape of hot code escapes the promotion story. Tiering notices heat between calls: the function is hot, so the next call runs optimised. That does nothing for a function called once that then spends thirty seconds inside one loop — there is no next call to upgrade.
On-stack replacement swaps the code underneath a running function. From V8 release v7.9, 20 November 2019:
V8 uses a technique called on-stack replacement (OSR) to install optimized code for the currently executing function. This allows us to start using the optimized code during the first execution of the function, while it is stuck in a hot loop.
The same post records a cost easy to miss: before v7.9 “we needed to re-optimize the function again in order to OSR it” — the OSR compile was thrown away and redone each time. Caching it, “keyed by the loop header that was used as the entry point in the OSRed function”, was worth a reported 5–18% on some peak-performance benchmarks. Speculative machinery accumulates its own caching problems on top of the caching it exists to provide.
What the speed costs
Three currencies, none hypothetical.
Memory. A tiered engine can hold several compiled representations of one function at once, plus the feedback making speculation possible. Sparkplug repurposes a stack slot specifically to cache the feedback vector — “the vector that stores object shape data, and needs to be loaded for most operations.” Shapes, transition trees, caches and multiple resident tiers are all memory spent on a bet.
Warm-up. Peak speed arrives only after enough executions to accumulate feedback, plus a compile, plus enough further executions to amortise it. A process that starts and exits pays the overhead and collects none of the payoff — the whole Hermes argument, made by people who could have shipped a JIT.
Engines soften warm-up with mechanisms that are not JIT tiers, and the two get sold under one label. V8’s startup snapshots (25 September 2015) deserialise a pre-built heap instead of rebuilding it — “just like thawing a frozen pizza for a quick dinner” — taking context creation “from 40 ms down to less than 2 ms” on desktop and “a difference between 270 ms and 10 ms” on an average phone, as measured in 2015. That removes work already done once. A novel hot loop still climbs the tiers from scratch in every fresh process.

Unpredictability. The speed of a piece of source is not a property of that source; it depends on what ran through it. Warm a function with one shape and introduce a second later, and it behaves differently than if both had arrived before promotion. A reviewer cannot see that in the code — which is why profile it is genuinely necessary here in a way it is not for an obviously quadratic loop.
Nobody in this story set out to speed up JavaScript
None of these ideas were invented for the web, and the gap between their publication and their arrival is the strangest fact in the subject.
Monomorphic inline caching comes from L. Peter Deutsch and Allan M. Schiffman, Efficient implementation of the Smalltalk-80 system, POPL 1984, pp. 297–302 (DOI 10.1145/800017.800542). ⚠️ That paper sits behind the ACM paywall, which returned HTTP 403 on 9 September 2026; its title, authors, venue and year are confirmed against an independent bibliographic record, and nothing is quoted from it here, because it was not loaded.
The extension that lets one site cache more than one shape is Hölzle, Chambers and Ungar’s polymorphic inline caches, ECOOP ’91:
Polymorphic inline caches (PICs) provide a new way to reduce the overhead of polymorphic message sends by extending inline caches to include more than one cached lookup result per call site.
The same paper names the second job an inline cache does, which is structurally the more important one: As an important side effect, PICs collect type information by recording all of the receiver types actually used at a given call site. The cache making today’s access fast is simultaneously the profiler telling the optimising compiler what to bet on tomorrow. Both halves of the tiering story descend from that sentence.
Hidden classes come from the same group two years earlier — Chambers, Ungar and Lee, OOPSLA ’89 — and the word V8 still uses internally is in the abstract:
To compensate for the absence of classes, our system uses implementation-level maps to transparently group objects cloned from the same prototype, providing data type information and eliminating the apparent space overhead for prototype-based systems.
A class-less, prototype-based, dynamically typed language needing a stable identifier for objects shaped like this — described and solved, in 1989.

Then, for roughly a decade, nothing. Allen Wirfs-Brock and Brendan Eich’s JavaScript: The First 20 Years (ACM HOPL, June 2020) is unsparing about the early engines: They did not utilize any of the sophisticated high performance implementation techniques that had been developed in the 1980s and early 1990s for Lisp, Smalltalk, Self, and other dynamic languages. Its explanation is resourcing rather than ignorance — For most of that period maintenance of a browser’s JavaScript engine was part-time work for a single software developer. The techniques were published, indexed and free. What arrived in 2008 was not a discovery; it was people who already knew, being paid to do it. The same paper records that Lars Bak, who led V8’s development, “built upon techniques he learned developing Smalltalk, Self, and Java virtual machines”.
What transfers
Strip the JavaScript out and the pattern stands bare: watch what actually happens, bet that the near future resembles the recent past, act before confirmation, and keep a correctness-preserving path back. That is not a runtime trick; it is what systems do wherever correctness and speed are both mandatory and the future is unknown.
| Speculative JIT | CPU branch prediction | Query plan caching | |
|---|---|---|---|
| Guesses | Types and shapes at a site | Which way a branch goes | Which access path is cheapest |
| Evidence | Feedback recorded by lower tiers | Recorded history of that branch | Table statistics, already slightly stale |
| A right guess wins | A cached offset instead of a lookup | A pipeline that never stalls | A plan reused instead of recomputed |
| A wrong guess costs | Deoptimisation, back to a lower tier | Pipeline flush, restart at the branch | A replan, or a badly chosen plan |
| Cold start | No feedback yet; interpreted | No history; predicts near chance | No statistics; a plan built on guesses |
The processor’s version is the closest match. It cannot wait to learn which way a branch goes, so it predicts from that branch’s recent history and runs ahead before confirmation. Right guess: real time banked. Wrong guess: discard the work and restart — architecturally the same event as a deoptimisation. The same three currencies apply. Predictor tables cost silicon and power whether or not a guess pays; a branch never seen before predicts about as well as a coin; and the identical binary runs at different speeds depending on the history preceding it.

Then, in January 2018, the bet turned out to carry a fourth cost nobody had priced. From the disclosure site published by the researchers who found it: Spectre breaks the isolation between different applications. It allows an attacker to trick error-free programs, which follow best practices, into leaking their secrets. The name is the point — The name is based on the root cause, speculative execution. The undo restored the architectural state faithfully. It did not undo what the discarded work left in the caches, and timing that residue leaks the data anyway.
That is the transferable lesson, and it is not about JavaScript. Any fast path doing real work before verifying a guess has to answer two questions, not one: is the undo correct, and is the undo total? Decades of engineers answered the first rigorously; nobody thought to ask the second until it was answered for them, in public.
Speculation earns its keep regardless — it is cheaper to build than certainty, and it is why a language that could not know its own types became fast. Just hold it as a purchase with a price rather than a free upgrade. The engines that build it publish the price; the engines that decline it publish why.
❓ Frequently Asked Questions
Does writing “monomorphic” code actually matter, or is it folklore? The mechanism is real and vendor-documented, so keeping a hot call site seeing one object shape is cooperation, not superstition. What is folklore is doing it everywhere: it pays where sites execute enormously often, and elsewhere trades readability for a difference you will not measure.
Is a JIT always faster than an interpreter? No, and the counterexample ships on hundreds of millions of phones. Hermes has no JIT by design, because its team measured startup time, binary size and memory rather than CPU benchmarks. For short-lived processes a JIT can lose outright: it pays the compile cost and exits before the speedup accrues.
What actually breaks a hidden class? Adding the same properties in a different order across instances; adding them after construction rather than in the constructor; deleting properties. Each forks the transition tree, so objects you think of as one type become several shapes and every cache betting on the old one starts missing.
Can I see a deoptimisation happen? Not from wall-clock timing, which is the trouble — a deopt-heavy function looks exactly like slow code. Engines expose deopt and inline-cache tracing behind diagnostic flags, and that instrumentation is the only thing separating this code is inherently expensive from the optimiser keeps abandoning this code.
Why does the same function run at different speeds on different runs? Because its speed is not a property of the source. It depends on which shapes reached each call site, in what order, and whether that happened before or after tier promotion. This is why microbenchmarks that ignore warm-up routinely measure the wrong thing.
Is four shapes really the megamorphic threshold in V8? Treat it as an illustration, not a constant. The figure comes from a former V8 engineer’s personal blog dated 11 January 2015, describing a tunable internal limit, and constants like it change between releases without announcement. Durable is the rule’s shape: a small fixed budget per site, and past it, per-site specialisation is abandoned.
📚 Recommended Resources
Disclosure: some links below are Amazon affiliate links. If you buy through them, this site earns a small commission at no extra cost to you.
Engine internals are one of the few subjects where the vendors publish better material than the publishers do, and they publish it dated. Start free.
Read the engine’s own blog, in order. v8.dev/blog is where every number in this article came from, and reading three or four posts in sequence teaches something no book can: how the same team’s reasoning changes across a decade. Sparkplug explains why more compilers beat a better compiler; Fast properties in V8 is the clearest published account of hidden classes anywhere; and Land ahoy: leaving the Sea of Nodes is a compiler team explaining, in public, why they threw away two of their own optimisers. Date every figure you take from it — these posts do not get retro-edited when the engine moves on.
Then read the 1980s papers, which are short and free. The Self bibliography hosts the abstracts and PDFs for both origin papers: polymorphic inline caches (Hölzle, Chambers and Ungar, ECOOP ’91) and the Self implementation paper that introduced “maps” (Chambers, Ungar and Lee, OOPSLA ’89). Both are shorter than most conference talks about them. ⚠️ The one paper you cannot read this way is Deutsch and Schiffman’s 1984 Smalltalk-80 paper, which is behind the ACM paywall.
And for how the language got here at all, Wirfs-Brock and Eich’s JavaScript: The First 20 Years is 189 peer-reviewed pages, free, and co-authored by the language’s designer. Its section on the pre-2008 engines explains the twenty-year delay better than any retrospective blog post.
Build one yourself, also free. Robert Nystrom’s Crafting Interpreters is readable in full online. You write two implementations of the same language — a tree-walking interpreter and then a bytecode virtual machine — and the second one makes the tiering argument in this article obvious rather than theoretical.
Books worth the shelf space
- Crafting Interpreters — Robert Nystrom, Genever Benning. The print edition of the free book above. Buy it if you intend to actually do the exercises; the hash-table, garbage-collector and bytecode chapters are the ones that make engine blog posts legible.
- Virtual Machines: Versatile Platforms for Systems and Processes — James E. Smith and Ravi Nair, Morgan Kaufmann. The standard reference on dynamic binary translation, profiling and adaptive optimisation — the general theory that JavaScript engines are one instance of. Pre-dates every engine named in this article, which is precisely why it explains them.
- Computer Architecture: A Quantitative Approach — John L. Hennessy and David A. Patterson, Morgan Kaufmann. For the other half of “What transfers”: branch prediction and speculative execution treated properly, with the measurement discipline that makes the JIT analogy more than a metaphor.
- Engineering a Compiler — Keith D. Cooper and Linda Torczon, Elsevier. The best single text on the optimisation passes a JIT’s top tier is choosing between. ⚠️ ASIN UNVERIFIED — the check digit for the third edition is arithmetically clean, but the identity could not be confirmed against an independent catalogue this session, so it is deliberately unlinked.