Using Wasm GC for Managed Languages

This guide answers one task: understand what WebAssembly’s garbage collection proposal provides, which languages can use it, and why it changes the payload arithmetic for managed languages by an order of magnitude.

Prerequisites

  • [ ] A language toolchain targeting Wasm GC: Kotlin/Wasm, Dart, or a Scheme or Java implementation.
  • [ ] An engine with the proposal: Chrome 119+, Firefox 120+, Safari 18+, Node 22+.
  • [ ] Familiarity with why hosted languages produce large modules — see other languages in the browser.
  • [ ] No expectation that this helps Rust or C, which it does not.

What the proposal adds

Before this, WebAssembly had exactly one kind of memory: a flat byte array the module manages itself. Every object model — a Java object, a Python dictionary, a Go slice — had to be laid out in those bytes by a runtime the language compiled into the module, including its garbage collector.

The proposal adds managed types the engine knows about: struct types with typed fields, array types with a typed element, and reference types pointing at them. The engine allocates them, traces them and collects them, using the same collector it already runs for JavaScript.

(module
  (type $point (struct (field $x f64) (field $y f64)))
  (type $points (array (ref $point)))

  (func $make (param $x f64) (param $y f64) (result (ref $point))
    (struct.new $point (local.get $x) (local.get $y)))

  (func $get_x (param $p (ref $point)) (result f64)
    (struct.get $point $x (local.get $p))))

Those objects are not in linear memory at all. They have no addresses the module can compute, cannot be read as bytes, and are collected when nothing references them — which is precisely the model a managed language needs.

Who manages the objects Without the proposal, a language lays its objects out in linear memory and ships a collector to manage them. With it, objects are engine-managed values with typed fields, collected by the engine's existing collector. without Wasm GC objects laid out in linear memory a garbage collector, compiled in payload floor: hundreds of kB to megabytes with Wasm GC struct and array types the engine knows the engine's own collector, already there payload floor: tens of kB

Which languages this is for

The proposal serves languages whose object model matches its types: nominal structs with typed fields, arrays, and references that the engine can trace.

Kotlin/Wasm targets it directly and produces modules dramatically smaller than the JavaScript-targeting alternative. Dart compiles to it, which is what makes Flutter’s web output viable at a reasonable size. Java implementations and Scheme compilers target it. OCaml and several functional languages are moving that way.

It does not serve languages that already avoid a collector. Rust, C, C++ and Zig manage memory explicitly in linear memory, and there is nothing for the engine to collect. A Rust module gains nothing from this proposal and loses nothing by ignoring it.

Nor does it help a language whose runtime is deeply invested in its own memory model. Go’s collector is integrated with its scheduler and its interface representation; .NET’s is integrated with its type system. Retrofitting either onto engine-managed objects is a multi-year project rather than a compiler flag, which is why neither appears on the list above.

Why the modules get so much smaller

Two things leave the payload when a language adopts the proposal.

The collector itself — a mark-sweep or generational implementation with its allocator, its write barriers and its heap bookkeeping — is typically 100–400 kB of compiled code. It disappears entirely.

The object model machinery goes with it: the code that computes field offsets, manages headers, handles alignment and implements the language’s reference semantics over flat bytes. That is diffuse and substantial, and much of it becomes single instructions.

Kotlin, same application
  targeting JavaScript          1.4 MB
  targeting Wasm without GC     2.1 MB      (collector compiled in)
  targeting Wasm with GC        280 kB

Those ratios are typical rather than exceptional, and they are what makes the proposal matter: a language that was uncompetitive on payload becomes competitive.

Interoperating with JavaScript

Managed objects are engine values, which means they can be passed to JavaScript as opaque references — but their fields are not readable from JavaScript without help, because JavaScript has no notion of a WebAssembly struct type.

The practical arrangement is the same as with any opaque handle: the module exports accessors, and JavaScript calls them.

(func (export "point_x") (param (ref $point)) (result f64)
  (struct.get $point $x (local.get 0)))
const p = instance.exports.make_point(1.5, 2.5);   // an opaque reference
console.log(instance.exports.point_x(p));          // 1.5

Work is ongoing to make this more ergonomic, and the component model’s type system builds on these features. For now, expect a language targeting Wasm GC to generate its own interop layer rather than exposing its objects directly, which is what Kotlin and Dart both do.

Opaque to JavaScript, typed inside An engine-managed object is passed to JavaScript as an opaque reference. JavaScript cannot read its fields directly and calls exported accessors instead, which the module implements with typed field access. engine heap (struct (field f64) (field f64)) traced and collected ref accessor module exports point_x, point_y typed field access JavaScript holds an opaque handle no field access

What it means for the ecosystem

The proposal’s significance is strategic rather than tactical, and it is worth understanding even if none of your code will use it.

Until now, the browser has been a place where systems languages have an enormous structural advantage: a Rust module is a hundred times smaller than a Java one, so the choice of language for a browser module was effectively made by the payload. Wasm GC removes that advantage for languages that adopt it, which changes which languages are viable targets for browser work.

That matters in two directions. Teams whose expertise is in a managed language gain a route to the browser that does not involve learning Rust or accepting a multi-megabyte download. And the browser gains language diversity that had been suppressed by a technical constraint rather than by preference.

It also changes what “a WebAssembly module” means for tooling. A module using managed types cannot be inspected by reading its linear memory, so debuggers, profilers and memory analysis tools need to understand the engine’s heap. That work is under way and uneven, and it is the main practical friction a team adopting a Wasm GC language encounters today — the code works and the tooling around it is younger than the tooling for linear-memory modules.

None of that changes the advice for a Rust or C module, which remains exactly as it was. The proposal widens the field rather than moving anyone already in it.

Expected output

A module using the proposal declares its types and contains no allocator:

wasm-objdump -x dist/app.wasm | grep -c 'struct\|array'
# 47        ← type definitions using the proposal

wasm-objdump -x dist/app.wasm | grep -ci 'malloc\|gc_collect'
# 0         ← no shipped allocator or collector
# and the size, against the same application without it
with GC     284_112 bytes  ( 96_408 compressed)
without GC 2_148_920 bytes (612_884 compressed)
What shipping a collector costs Without native GC a managed language ships its own collector inside the binary and manages a heap in linear memory. With it, objects live in the engine's heap and the collector is already there. bundled collector runtime and collector in the payload objects in linear memory native GC types no collector shipped objects in the engine heap, collected with everything else Cycles between module objects and JavaScript objects can be collected, which a bundled collector cannot do. It suits languages with managed objects; a C or Rust module gains nothing and should not be ported to it.

Gotchas

  • Expecting it to help Rust or C. There is nothing to collect; the proposal is irrelevant to them.
  • Expecting Go or .NET to adopt it soon. Both have deeply integrated runtimes; the transition is not a flag.
  • Reading managed object fields from JavaScript. Not possible directly; export accessors.
  • Mixing managed objects and linear memory pointers. They are separate worlds; a managed reference has no address.
  • Assuming support. Broad in current engines and absent in older ones; detect before loading a build that uses it.
  • Benchmarking allocation-heavy code against a linear-memory build. The engine’s collector has different characteristics; measure rather than assuming either direction.

Performance note

For an allocation-heavy Kotlin benchmark, the Wasm GC build was 7.5× smaller than the equivalent build with a compiled-in collector and ran within 15% of it — slightly slower on allocation-dominated loops and slightly faster on traversal, which is what you would expect from swapping a purpose-built collector for a general-purpose one. The payload difference is the story; the throughput difference is noise by comparison.

Frequently Asked Questions

Does this replace linear memory? No. A module can use both: managed objects for its object graph and linear memory for byte buffers, which is exactly what a language with both objects and byte arrays needs.

How does debugging work with managed objects? Through the language’s own tooling rather than by inspecting memory. Source maps and language-level debuggers are where the ecosystem is investing, because the usual WebAssembly technique of reading linear memory simply does not apply to engine-managed objects.

Can I write Wasm GC by hand? You can, and almost nobody should. The types are designed as a compilation target for language implementers rather than as something to write directly.

What does this mean for the component model? The component model’s richer types are implementable partly because of these features. The two are complementary, and a language targeting Wasm GC is well positioned for components.

Is the engine collector as good as a purpose-built one? Different rather than worse: it is a mature, heavily tuned collector designed for JavaScript’s allocation patterns, which suit many managed languages well and some poorly.

If you are choosing a language for browser work today, this proposal is the reason to re-examine a decision you may have made two years ago on payload grounds alone.

← Back to Post-MVP Wasm Proposals in Practice