TL;DR — Cranelift is a fast, multi-tier JIT compiler purpose-built for WebAssembly and used in production by Wasmtime and Fastly Compute. It trades peak single-thread performance for predictable, low-pause compilation, which is exactly what runtime and edge systems need. This post walks through its pipeline, the optimizations that matter at the baseline tier, and how it stacks up against LLVM and V8’s TurboFan.
Why WebAssembly Needs Its Own JIT
WebAssembly (Wasm) was designed to be a portable, deterministic compilation target for the web. By 2019, the same properties that made it good for browsers — sandboxed execution, fast cold starts, predictable performance — made it attractive well beyond the browser. Today Wasm is the runtime substrate for Fastly Compute, Cloudflare Workers, Cosmonic wasmCloud, and the Wasmtime server-side runtime.
But running Wasm efficiently is not the same as running JavaScript efficiently. Wasm modules are statically typed, structured-control-flow validated, and free of hidden allocation. That makes optimization different. You do not need type feedback or speculation; you do need to compile fast, because most Wasm modules are short-lived, especially at the edge. A request handler that takes 30ms to JIT has already lost the latency budget.
This is the niche Cranelift fills. It is a code generator that compiles Wasm (and any frontend that targets its IR) directly to machine code, with compilation pauses that are an order of magnitude shorter than LLVM. It is not the fastest possible compiler — V8’s TurboFan and Cranelift’s own optimizing tier both beat it on peak throughput — but it is the right compiler for the first request, and that is what production Wasm runtimes care about.
Cranelift’s Place in the Toolchain
Cranelift started inside the CraneLift project as a successor to the old single-pass SpiderMonkey baseline. It was rewritten for Wasmtime and now lives under the Bytecode Alliance as a reusable code generator with multiple frontends:
- Wasmtime uses Cranelift as its primary compilation backend, with an experimental optimizing tier built on the same IR.
- Fastly Compute uses the Wasmtime stack, so every edge function on Fastly’s network runs through Cranelift at the baseline.
- Wasmer historically supported Cranelift as an alternative backend before consolidating on LLVM and a Cranelift-derived codegen.
- Rust uses Cranelift as a backend for
rustcvia the unstablerustc_codegen_craneliftcodegen, mostly for faster debug builds.
Crucially, Cranelift is not a frontend. It consumes CLIF (Cranelift Intermediate Format), a low-level, SSA-based, machine-agnostic IR. Wasmtime’s Wasm frontend lowers validated modules into CLIF, then Cranelift turns CLIF into x86_64, aarch64, s390x, or riscv64 machine code. That separation lets Cranelift be a backend for any IR-producing frontend, not just Wasm.
The Cranelift Compilation Pipeline
Cranelift’s pipeline is deliberately short. The whole point is that you can compile a function and emit code before a user notices.
┌──────────────┐ ┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐
│ Wasm binary │ → │ Validate │ → │ Translate │ → │ Simplify │ → │ Regalloc │ → │
│ (validated) │ │ (done) │ │ Wasm → CLIF│ │ & folds │ │ & layout │ │
└──────────────┘ └────────────┘ └──────────────┘ └────────────┘ └────────────┘
│
▼
┌──────────────┐
│ Emit machine│
│ code │
└──────────────┘
A few things stand out compared to a traditional AOT pipeline like clang -O2:
- No separate middle-end. Cranelift folds simple rewrites (constant folding, strength reduction, dead-code elimination) directly during translation. There is no separate GVN, inliner, or loop vectorizer pass.
- No inliner. Function calls in Wasm are resolved at link time and become direct calls; there is no inlining at the Wasm level because the optimizer cannot see across module boundaries in a meaningful way for cold-start reasons.
- One linear scan register allocator. Cranelift does not use graph coloring. The trade-off is described in detail in the regalloc.rs implementation notes.
For an edge platform compiling 30,000 Wasm modules per second during a traffic spike, those trade-offs are not compromises — they are the design.
Translating Wasm to CLIF
The translation step is where most of the “free” Wasm wins come from. Wasm’s design gives Cranelift information that a JS JIT has to reconstruct.
Stack machine → SSA
Wasm is a structured stack machine: instructions pop operands from an implicit stack and push results. Cranelift’s translator, in cranelift/wasm/src/code_translator.rs, maintains an explicit stack of SSA values. Each Wasm instruction is lowered into one or more CLIF instructions with the top of the stack as arguments and the results pushed back onto the translator stack. Because Wasm’s structured control flow is already validated, the translator does not need to recover CFG edges — they are explicit.
Types are real, not hints
Every local, global, and function parameter has a concrete type: i32, i64, f32, f64, v128 (SIMD), or a reference type. Cranelift’s IR is also typed, so there is no Value boxing, no hidden class transitions, and no polymorphic inline caches. An i32.add becomes a CLIF iadd and from there an ADD or ADD r/m32 on x86 with no speculation.
Calls are typed
Wasm function calls carry their signature in the module. Cranelift can emit a direct call with the right ABI from the very first execution — no megamorphic dispatch site, no inline cache warmup. The only thing it cannot do at the baseline tier is optimize across call boundaries, which is what the optimizing tier exists for.
Baseline Optimizations That Matter
Cranelift’s optimization story is “do the cheap things well and stop.” Here are the passes that actually move the needle at the baseline tier.
Constant folding and algebraic simplification
Cranelift folds constants at translation time. A Wasm module doing i32.const 2; i32.const 3; i32.add; i32.const 4; i32.mul lowers to i32.const 20 if the operations are in the same basic block and the optimizer’s dataflow confirms the inputs are constants. The simplify module also does algebraic identities: x * 1 → x, x + 0 → x, x ^ x → 0, and the usual suspects.
Bounds-check elimination for memory accesses
Wasm memory operations carry an offset and a bounds check. Cranelift tracks which checks are statically redundant and elides them. A Wasm loop that walks a fixed array of 16 i32s with a constant base and a loop counter that the translator can prove stays in bounds will emit zero checks in the steady state. This pass is the reason tight numeric kernels in Wasm can be within ~10% of native C performance.
Dead-code and unreachable-block elimination
Wasm’s validation guarantees that unreachable code is unreachable. Cranelift exploits this aggressively: the first instruction in an unreachable block terminates the block, so any instructions after it can be dropped during translation. This is small in isolation but compounds across thousands of modules.
Branch hinting and layout
Cranelift uses profile data only via the tier-up mechanism — at the baseline tier it has none. Instead, it relies on layout: cold blocks (exception handlers, panic paths) are placed out of line, and forward branches default to “not taken” so that the CPU’s static predictor handles them sensibly until profile-guided feedback arrives.
Stack slots for locals
Every Wasm local becomes either a register or a stack slot in Cranelift’s frame. The translator picks stack slots for values that are pinned (escaped, address-taken, or large), and registers for everything else. The regalloc then promotes stack slots to registers when possible. This is essentially what every JIT does; the difference is that Cranelift does it in one pass during emission rather than after a separate coloring phase.
Register Allocation: Linear Scan, Intentionally
Cranelift uses a linear scan allocator, specifically the variant described by Wimmer and Mössenböck. The reasoning matters:
- Compile time. Linear scan is O(n) in the number of live ranges. Graph coloring is closer to O(n²) in the worst case and has backtracking. For a compiler whose goal is sub-millisecond compilation per function, linear scan is the right shape.
- Code quality. Modern linear scan implementations are within a few percent of coloring allocators on typical code, especially when combined with SSA-based live range splitting (which Cranelift does).
- Predictability. The allocator’s behavior is much easier to reason about, which matters when you are debugging a JIT for production.
The allocator works on SSA-form CLIF. Live ranges are split at critical edges before register allocation, so a range that crosses a loop header can be split into a “back-edge” copy that lives in a register and a “fall-through” copy that may spill. The split is encoded directly in the IR with move and copy instructions, which the allocator then has to honor.
Tiering Up: When Cranelift Stops Being Enough
Cranelift is the baseline JIT, but Wasmtime also ships an optimizing compiler built on the same IR. The optimizing tier adds:
- Inlining of Wasm-to-Wasm calls, which Cranelift deliberately does not do.
- GVN and LICM, which the baseline skips to keep compilation fast.
- More aggressive loop transforms, including unrolling and rotation.
The tier-up decision happens at runtime. Wasmtime samples execution counts per function via lightweight instrumentation in the baseline code. When a hot function crosses a threshold (configurable, default 10,000 calls in a typical deployment), it is recompiled by the optimizing tier and the patched in place. The instrumentation overhead is small enough that the tiering decision itself is essentially free.
This two-tier shape is the same pattern V8 uses for Sparkplug → TurboFan, and SpiderMonkey uses for Baseline → WarpBuilder → Ion. The difference is that Cranelift’s two tiers share an IR and many codegen components, which keeps the maintenance surface small.
Production Patterns: What Cranelift Gets Right
Cold start
A 1 MB Wasm module typically compiles in single-digit milliseconds with Cranelift, where the same module with LLVM takes 50–200 ms. For serverless and edge platforms, that is the difference between a 5 ms P50 and a 50 ms P50 on a cold invocation. Fastly’s Compute@Edge platform explicitly leans on this — functions are expected to be short-lived, and the entire platform design assumes that compilation fits inside the request’s first-byte budget.
Determinism
Cranelift’s output is deterministic. Same inputs in, same machine code out, byte-for-byte. This matters for caching: a Wasmtime instance can memoize compiled artifacts and share them across processes. It also matters for security auditing — deterministic code makes it easy to verify that a module compiles to what you expect.
Memory model
Cranelift emits code that respects Wasm’s memory model exactly. There are no surprise fences or relaxed atomics: Wasm threads are mapped to the host’s atomic primitives, and Cranelift emits the right LOCK-prefixed or LDAR/STLR sequences depending on the operation. The semantics are described in the Wasm threads proposal and Cranelift honors them without speculation.
SIMD
The relaxed-simd and simd proposals are first-class in Cranelift. The codegen lowers Wasm’s 128-bit v128 operations directly to SSE, AVX, or NEON depending on the target’s features. Cranelift’s SIMD story is significantly ahead of most JS engines’ because Wasm gives it static type and shape information — there is no Float64Array polymorphism to disambiguate at runtime.
Compact code
Cranelift prioritizes code size less than LLVM’s -Os but more than -O3. For Wasm at the edge, code size matters because it is shipped across the network to the runtime. Cranelift keeps cold blocks compact and uses small encoding tricks (like merging adjacent constant pools) that LLVM does not bother with at higher optimization levels.
Where Cranelift Falls Short
No honest post would skip the trade-offs.
- Peak single-thread performance is lower than LLVM. A tight numeric loop compiled by Cranelift can be 1.5–2× slower than the same loop compiled by
clang -O3. The optimizing tier narrows this but does not close it. - No autovectorization across basic blocks. Cranelift vectorizes within basic blocks when the types line up but does not perform loop vectorization. The SIMD story is “what the frontend can express in Wasm SIMD ops,” not “what we can lift out of scalar Wasm.”
- Cross-module optimization is limited. Wasm’s module boundary is sacred; Cranelift cannot inline across modules even when both are loaded into the same runtime. There is ongoing work on component-model optimization that may change this.
- Profile-guided optimization is rudimentary. The tier-up mechanism is profile-driven, but Cranelift itself does not consume PGO data at compile time. The tier-up compiler does some of this, but it is nowhere near what GCC’s PGO can do.
For workloads where these matter (tight CPU-bound numerics, large long-lived modules), an ahead-of-time LLVM pipeline is the better choice. For everything else — short-lived functions, edge compute, plugin systems, sandboxed user code — Cranelift is exactly right.
Key Takeaways
- Cranelift is a baseline JIT designed for low compilation latency, not peak throughput — and that is exactly what production Wasm runtimes need.
- It consumes a typed, SSA IR (CLIF) and emits machine code directly, with no separate middle-end, inliner, or heavy register-allocation pass.
- The optimizations that matter at the baseline tier are cheap and high-leverage: constant folding, bounds-check elimination, dead-code elimination, and good register allocation.
- Wasmtime’s two-tier architecture (Cranelift baseline + Cranelift optimizing) gives most of the benefits of an AOT pipeline with the cold-start latency of an interpreter.
- Cranelift is a strong fit for edge and serverless workloads where compile time dominates, and a weaker fit for long-lived, CPU-bound workloads where LLVM’s peak performance matters more.