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.
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.
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)
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.
Related
- Reference types and externref — the earlier proposal this builds on.
- Comparing payload size across languages — the numbers this changes.
- Other Languages in the Browser — which group each language is in.
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