Performance
Zena is designed to deliver predictable, near-native execution speed on WebAssembly GC.
This performance profile comes from two complementary factors: and hidden runtime checks simply by omitting those features from the language.
What each construct costs ​
Understanding how language features translate to WebAssembly instructions helps you make informed architectural choices.
| Cost Category | Language Construct | WebAssembly Lowering | Runtime Impact |
|---|---|---|---|
| Zero-Cost | Primitive operations (+, *, ==) |
i32.add, f64.mul, i32.eq |
Raw hardware ALU speed; no boxing or tagging. |
| Zero-Cost | Inline tuples (inline (a, b)) |
(result T_A T_B) |
Kept on the execution stack/registers; zero GC allocations. |
| Zero-Cost | Extension class methods | Direct (func $name ...) |
Erased at compile time; receiver passed as first argument. |
| Zero-Cost | final methods and direct calls |
Direct call $func |
Direct call target; eligible for host engine inlining. |
| Zero-Cost | Top-level function declarations |
Plain (func ...) |
Zero closure environment allocation. |
| Low-Cost | Class field access (obj.field) |
struct.get $Class $offset |
Single-instruction direct offset load. |
| Low-Cost | Class construction (new Point(...)) |
struct.new $Point |
Single-instruction allocation with pre-evaluated fields. |
| Low-Cost | Virtual method calls | struct.get + call_ref |
Single load from static vtable followed by indirect call. |
| Moderate | Interface method calls | Fat Pointer ITable lookup | Two-word indirection (load ITable from fat pointer, then call_ref). |
| Moderate | Arrow functions / Closures | struct.new $Closure |
Allocates a GC closure struct to capture local variables. |
| Moderate | Function arity adaptation | Adapter trampoline | Lightweight wrapper when passing a callback with fewer arguments. |
| Allocation | Explicit boxing (new Box(42)) |
struct.new $Box |
Heap allocation to wrap primitive in a reference type. |
| Allocation | GrowableArray<T> resizing |
array.copy + array.new |
Reallocates backing buffer when exceeding capacity. |
Monomorphized generics ​
Zena uses generic monomorphization (specialization) rather than runtime
generic erasure to Object or anyref.
When you write a generic class or function:
class Cell<T>(value: T)
let intCell = new Cell<i32>(42);
let floatCell = new Cell<f64>(3.14);
The compiler synthesizes distinct, specialized WebAssembly types and functions for each unique concrete type argument:
;; Specialized struct for Cell<i32>
(type $Cell_i32 (struct (field $value i32)))
;; Specialized struct for Cell<f64>
(type $Cell_f64 (struct (field $value f64)))
Benefits of monomorphization ​
- Unboxed primitive collections: An
Array<i32>is backed by a native Wasm(array i32). Reading or writing elements requires no boxing or unboxing conversions. - Direct struct layout: Generic fields containing primitives occupy exact
scalar bitwidths (e.g., 4 bytes for
i32, 8 bytes forf64) without pointer indirection. - Specialized operations: Arithmetic inside generic functions compiles to native hardware instructions rather than virtual method dispatch.
Managing specialization overhead ​
Monomorphizing generics creates a separate copy of code and type declarations for each concrete type instantiation. To keep binaries compact:
- The compiler's reachability analysis only emits specializations that are actually instantiated in the program.
- Shared logic that does not depend on type parameters can be factored into non-generic base classes or helper functions.
Devirtualization and inlining ​
Virtual method dispatch in object-oriented programming introduces a level of indirection by loading a function reference from a vtable before calling it.
Zena applies compile-time devirtualization to eliminate this indirection
wherever possible, replacing virtual dispatches with direct WebAssembly
call $func instructions.
Explicit devirtualization with final ​
Declaring a class or method as final informs the compiler that it cannot be
overridden:
final class FastCalculator {
compute(x: i32): i32 {
return x * 2 + 1;
}
}
Calls to FastCalculator.compute bypass vtable lookups completely. Direct calls
are prime candidates for host JIT compilers (such as V8 or Wasmtime) to inline
the method body entirely.
Whole-program devirtualization ​
Even when a class is not explicitly marked final, Zena's compiler performs
whole-program reachability analysis during compilation. If the compiler proves
that a class has no derived subclasses instantiated anywhere in the application,
it automatically devirtualizes calls on that class.
Boxing and how to avoid it ​
In many languages, boxing occurs implicitly whenever primitive values are passed to generic APIs, collections, or interfaces.
In Zena, primitives are never implicitly boxed.
Soundness without a universal any ​
Zena has no universal any or Object type that can transparently hold both
primitives and references. anyref is the top type for references only.
If code requires putting a primitive value behind a reference type, it must be
explicitly wrapped using Box<T>:
class Box<T> {
value: T;
new(this.value);
}
let boxed: anyref = new Box<i32>(42);
Because boxing requires an explicit new Box(...), allocations are never hidden
inside language syntax.
Best practices for avoiding allocations ​
- Use monomorphic collections: Use
Array<i32>orFixedArray<f64>instead of generic wrapper types. - Use inline tuples for multi-value returns: Return multiple values using
inline (A, B)to leverage unboxed WebAssembly multi-value returns:zenafunction divide(a: i32, b: i32): inline (i32, i32) { return (a / b, a % b); // Stored in registers; zero heap allocations } - Prefer value parameters in extension classes: Extension classes on
primitives (
extension class IntOps on i32) are completely erased at compile time with zero object wrapper overhead. - Use concrete array types in hot loops: Because
Array<T>is an interface, accessing elements viaArray<T>incurs Fat Pointer interface dispatch. Typing variables or parameters as concrete classes (FixedArray<T>orGrowableArray<T>) allows direct method dispatch and inlining in tight loops.
What ends up in the binary ​
Zena produces compact, standalone WebAssembly modules containing only the code required to execute the application.
A compiled .wasm file consists of:
- Type section: Nominal declarations for every reached class struct, interface vtable, closure environment, and array type.
- Function section: Compiled WebAssembly bytecode for all reached methods, functions, and synthesized initializers.
- Global section: Static singletons, immutable vtables, and constant string references.
- Element section: Function pointer tables used for indirect vtable calls.
- Data section: Static string bytes and binary data literals.
Impact of debug metadata (-g) ​
By default, production builds strip all symbol names and debug metadata to minimize binary size.
When compiling with the -g flag, the compiler attaches a WebAssembly name
section containing original function, class, and method names. While
invaluable for debugging and stack traces, the name section adds metadata
bytes and should be omitted in size-critical production deployments.
Dead code elimination ​
Zena utilizes a whole-program Rapid Type Analysis (RTA) pass during compilation to eliminate unused code and types.
How reachability analysis works ​
- Rooting entry points: Compilation begins by marking the main entry point
function (
export function main(...)) or public library exports as live. - Propagating references: As functions are analyzed, any classes constructed, methods called, or fields read are added to the reachable set.
- Pruning unused members: Methods in imported libraries or unused classes that are never instantiated or invoked are completely stripped from the final WebAssembly binary.
- Pruning type declarations: Any Wasm GC struct or array types that are never constructed or referenced are omitted from the module's type section.
This whole-program analysis allows developers to import comprehensive standard library modules without incurring code size penalties for features they do not use.
Measuring and benchmarking ​
To measure and optimize Zena applications, use the built-in benchmarking tools and inspection utilities.
Running benchmarks ​
The Zena repository includes a micro-benchmarking suite that compares Zena's execution performance against native JavaScript under Node.js and Wasmtime:
npm run benchmark -w @zena-lang/zena-compiler
To filter for specific benchmarks:
npm run benchmark -w @zena-lang/zena-compiler -- --filter StringBuilder
Inspecting WebAssembly bytecode ​
To inspect the generated WebAssembly bytecode and verify compiler optimizations:
# Compile to WebAssembly Text format (.wat)
npm run zena -w @zena-lang/zena-cli -- build main.zena -o main.wat
# Inspect section sizes using wasm-tools
wasm-tools objdump main.wasm
Next ​
- WebAssembly — type mapping, memory layout, and runtime architecture
- Values and Variables — immutability and variable semantics
- Classes — case classes, vtables, and devirtualization