• Terms of Service

Learn Internet Grow

Tech Skills

  • Learn
  • Books
  • Search
  • Create
  • Measure
  • Secure
  • Share
  • Terms of Service

JavaScript JIT Compilation: How an Engine Guesses, and How It Takes the Guess Back

Filed Under: Create

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?

A causeway being built forward into fog with wheel tracks already on the new planks, and a single taut glowing cable running back to solid stone

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.

Four tools of increasing size along a workbench, dozens of rough pieces near the small ones and a single mirror-polished piece under the largest press

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 stateWhat that one site has seenOn the next hitWhat it costs you
MonomorphicExactly one shape, everOne comparison, then a cached offset or targetNothing — this is the fast path
PolymorphicA small number of distinct shapesCheck candidates in sequence until one matchesSlower than one comparison, far better than a full lookup
MegamorphicToo many shapes to track individuallyGive up per-site caching; fall back to a shared tablePer-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.

Three worn stone thresholds, the first with one deep polished groove holding light, the second with several shallow crossing grooves, the third scuffed evenly with none

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.

An open iron mould with two identical castings before it and a third casting that has had a lump welded on afterwards, so the mould can no longer close over it

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.

An empty skiff far out on black night water, arrested mid-run by a glowing line that has just gone taut back to a heavy stone mooring on shore

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.

Three large glass vessels of oil drained to different levels, all feeding one small bright flame that lights only a modest circle

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.

A dusty vaulted archive with one lamp burning over an open book and a single fresh track of footprints arriving after decades of undisturbed dust

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 JITCPU branch predictionQuery plan caching
GuessesTypes and shapes at a siteWhich way a branch goesWhich access path is cheapest
EvidenceFeedback recorded by lower tiersRecorded history of that branchTable statistics, already slightly stale
A right guess winsA cached offset instead of a lookupA pipeline that never stallsA plan reused instead of recomputed
A wrong guess costsDeoptimisation, back to a lower tierPipeline flush, restart at the branchA replan, or a badly chosen plan
Cold startNo feedback yet; interpretedNo history; predicts near chanceNo 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.

An immaculately cleared dark stone table with a single faint ring of residual warmth still glowing where a vessel had stood

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.

Newsletter

Tags

Agentic AI Agentic Development AI AI Agents AI Coding Agents Artificial intelligence Backlinks Blockchain Bounded Context Browser ChatGPT Plugins Claude Code CLPR Context Engineering Cross-Chain Cryptocurrency Developer Tools Distributed Systems DLT Domain-Driven Design Email Google Search HBAR Hedera Hedera Hashgraph Internet LLM Machine Learning MCP Microservices Model Context Protocol News NLP Orchestration Photoshop Productivity Saga Pattern SEO Smart Contracts Software Architecture software engineering Twitter Typescript Web3 Writing

Recent Articles

  • QUIC and HTTP/3: The Protocol That Won Is Being Routed Around October 11, 2026
  • The Mark I Perceptron: The Machine That Learned to See October 11, 2026
  • GraphRAG vs Vector RAG: Knowledge Graphs for AI, Explained October 11, 2026
  • Cardano Tokenomics: How ADA’s Supply Actually Works October 10, 2026
  • Transformer Encoder vs Decoder: How to Read the Diagram October 10, 2026
  • Nobody Checks the Verified Badge: The Real State of Git Commit Signing October 10, 2026

Copyright © 2026 · News Pro Theme On Genesis Framework · WordPress · Log in

Content on this site is produced with the assistance of Large Language Models and edited by a human. It is intended for educational purposes — facts may occasionally be incorrect. If you spot an error, please email Larionovdmitri@learninternetgrow.com.

Affiliate Disclosure: This site contains affiliate links to Amazon and other partners. If you purchase through these links, LearnInternetGrow.com may earn a commission at no additional cost to you. We only recommend products we genuinely believe in.

Privacy Policy · Terms of Service