{"schemaVersion":1,"id":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes","title":"WASM GC: Garbage-Collected Reference Types in WebAssembly","description":"The WASM GC proposal brings struct and array types with automatic garbage collection to WebAssembly. This article examines its impact on Kotlin/WASM, Dart/WASM, and browser runtime architecture.","language":"en","datePublished":"2026-01-20","dateModified":null,"contentFormat":"text/markdown","content":"WebAssembly\n\n# WASM GC: Garbage-Collected Reference Types in WebAssembly\n\n2026-01-208 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/wasm-gc-garbage-collection-browser-runtimes>)\n\n## The Problem WASM GC Solves\n\nCompiling a garbage-collected language like Kotlin, Dart, or OCaml to WebAssembly has historically been painful. The traditional approach was to compile your language's runtime, including its entire garbage collector, into a WASM module. That GC would then manage objects inside WASM's linear memory, a flat byte array that the host runtime knows nothing about.\n\nThis worked, but it was terrible:\n\n- **Bloated binaries**: You're shipping a full GC implementation (often 200-500KB) in every module.\n- **No integration with the host GC**: The browser's V8 or SpiderMonkey collector has no idea what's alive or dead in your linear memory. Two GCs running independently, completely unaware of each other.\n- **No cycle collection across boundaries**: If your WASM object holds a reference to a JS object and vice versa, neither GC can collect the cycle.\n- **Redundant work**: The browser already has a world-class, highly-optimized garbage collector. Why bring your own?\n\n## What WASM GC Adds\n\nThe proposal introduces several new type constructs and instructions. The following sections cover the core additions.\n\n### Struct Types\n\n```wasm\n;; Define a struct type for a linked list node\n(type $Node (struct\n (field $value i32)\n (field $next (ref null $Node))\n))\n\n;; Create a new node\n(struct.new $Node\n (i32.const 42) ;; value\n (ref.null $Node) ;; next = null\n)\n\n;; Read the value field\n(struct.get $Node $value) ;; returns i32\n```\n\nThese structs live on the host GC's heap. When nothing references them, the browser's collector reclaims them. No custom allocator, no free lists, no manual memory management.\n\n### Array Types\n\n```wasm\n;; A growable array of i32\n(type $IntArray (array (mut i32)))\n\n;; Create an array of 100 elements, all initialized to 0\n(array.new $IntArray\n (i32.const 0) ;; init value\n (i32.const 100) ;; length\n)\n\n;; Get element at index 5\n(array.get $IntArray (i32.const 5))\n\n;; Set element at index 5\n(array.set $IntArray (i32.const 5) (i32.const 999))\n```\n\n### Type Hierarchies and Casts\n\nWASM GC supports structural subtyping:\n\n```wasm\n(type $Shape (struct\n (field $x f64)\n (field $y f64)\n))\n\n(type $Circle (sub $Shape (struct\n (field $x f64)\n (field $y f64)\n (field $radius f64)\n)))\n\n;; Downcast a Shape ref to a Circle ref\n(ref.cast (ref $Circle)\n (local.get $shape_ref)\n)\n\n;; Test if a ref is a Circle (returns i32 0 or 1)\n(ref.test (ref $Circle)\n (local.get $shape_ref)\n)\n```\n\nThe ` i31ref ` type is another gem, it packs a 31-bit integer into a reference, which is perfect for tagged values that languages like OCaml and Scheme rely on.\n\n## Kotlin/WASM: The First Real Production Story\n\n[JetBrains](<https://www.jetbrains.com/>) has invested heavily in Kotlin/WASM GC, and the results are impressive. The Kotlin/WASM compiler (K2 backend) now targets WASM GC by default, and the difference from the old linear-memory approach is substantial.\n\nThe following Kotlin class and its compiled output illustrate the mapping:\n\n```kotlin\nclass LinkedList<T>(\n val head: T,\n val tail: LinkedList<T>? = null\n) {\n fun length(): Int {\n var count = 0\n var current: LinkedList<T>? = this\n while (current != null) {\n count++\n current = current.tail\n }\n return count\n }\n}\n```\n\nThe Kotlin/WASM compiler maps this to WASM GC structs:\n\n```wasm\n(type $LinkedList (struct\n (field $head (ref $Object))\n (field $tail (ref null $LinkedList))\n (field $vtable (ref $LinkedList_vtable))\n))\n```\n\nNo separate heap. No custom allocator. The browser's GC handles it all.\n\n### Binary Size Comparison\n\nTesting a medium-complexity Kotlin Compose Multiplatform app (a task manager with about 15 screens):\n\n| Approach | Binary Size | Load Time (3G) | Peak Memory |\n| --- | --- | --- | --- |\n| Kotlin/WASM (old, linear mem + GC) | 2.8 MB | 4.2s | 48 MB |\n| Kotlin/WASM GC | 680 KB | 1.1s | 22 MB |\n| Kotlin/JS | 1.4 MB | 2.3s | 35 MB |\n\nThat's a **75% reduction** in binary size and a **54% reduction** in peak memory. The load time improvement mostly comes from not having to initialize a custom GC at startup.\n\n## Dart/WASM: Flutter's Web Story Gets Real\n\nThe [Flutter](<https://flutter.dev/>) team has been working on Dart-to-WASM compilation, and WASM GC is the missing piece that makes it viable. Dart 3.3+ compiles to WASM GC, and Flutter Web can now use it in production.\n\n```dart\n// Dart class hierarchy\nabstract class Animal {\n final String name;\n Animal(this.name);\n String speak();\n}\n\nclass Dog extends Animal {\n Dog(String name) : super(name);\n\n @override\n String speak() => '$name says woof!';\n}\n\n// This compiles to WASM GC struct types with subtyping\n// Dog <: Animal, with vtable dispatch for speak()\n```\n\nThe Dart compiler maps class hierarchies directly to WASM GC's structural subtyping. Virtual method dispatch uses ` ref.cast ` and function references, no more vtable trampolines through linear memory.\n\nPerformance numbers from a Flutter Web benchmark (Wonderous app):\n\n| Metric | dart2js | Dart/WASM GC |\n| --- | --- | --- |\n| Frame time (avg) | 18ms | 8ms |\n| Frame time (p99) | 45ms | 14ms |\n| Initial load | 3.1s | 1.8s |\n| Memory (steady state) | 62 MB | 31 MB |\n\nThe p99 frame time improvement is the one that matters most. The long GC pauses that caused poor Flutter Web frame rates are largely eliminated because the browser's incremental collector is doing the work now instead of a stop-the-world collector running inside linear memory.\n\n## Performance Analysis: Strengths and Limitations\n\nBenchmarking WASM GC across different workloads reveals a nuanced picture. It is not universally faster.\n\n### Where WASM GC Wins\n\n**Object-heavy workloads**, anything that allocates and discards lots of small objects. The browser's generational GC is incredibly optimized for this pattern. A tree-building benchmark:\n\n```javascript\n// Benchmark: build and traverse a binary tree of depth 20\n// 2^20 = ~1M nodes\n\n// WASM GC (Kotlin/WASM): 145ms average, 12ms p99 GC pause\n// Linear memory (C++ custom allocator): 89ms average, 0ms GC pause\n// Linear memory (Kotlin old): 210ms average, 85ms p99 GC pause\n// JavaScript: 320ms average, 25ms p99 GC pause\n```\n\nWASM GC beats the old Kotlin/WASM approach by 30%, and while raw C++ is still faster (no GC overhead at all), the gap is much smaller than you'd expect.\n\n### Where WASM GC Loses\n\n**Tight numeric loops**, if you're doing matrix math or signal processing, WASM GC structs add indirection that linear memory doesn't have. You're going through reference types instead of reading raw bytes at known offsets.\n\n```wasm\n;; Linear memory: direct load from known offset\n(f64.load (i32.add (local.get $base) (i32.const 16)))\n\n;; WASM GC: field access through reference\n(struct.get $Matrix $m02 (local.get $mat_ref))\n```\n\nIn a matrix multiplication benchmark (1024x1024, f64), the WASM GC version was about 15-20% slower than linear memory. Not catastrophic, but measurable.\n\n## Cross-Module GC References: The Integration Story\n\nThe really transformative part is how WASM GC objects integrate with JavaScript. A WASM GC struct reference can be passed directly to JavaScript as an opaque ` WebAssembly.Struct ` object. The browser's GC sees both the JS wrapper and the WASM struct as part of the same object graph.\n\n```javascript\n// JavaScript side\nconst wasmModule = await WebAssembly.instantiateStreaming(\n fetch('app.wasm'),\n imports\n);\n\n// Get a GC-managed struct from WASM\nconst node = wasmModule.exports.createNode(42);\n\n// Store it in a JS data structure\nconst cache = new Map();\ncache.set('node1', node); // JS GC tracks this reference\n\n// When cache is cleared, the WASM struct becomes eligible for collection\n// No leak! The host GC sees the full reference graph.\n```\n\nThis was literally impossible with linear memory. You had to manually pin objects and create weak reference tables to avoid leaks. These leaks in production environments are notoriously among the worst bugs to track down.\n\n## What's Still Missing\n\nWASM GC isn't complete. A few notable gaps remain:\n\n1. **Weak references**: Not in the current spec. Languages that rely on weak refs (Java's ` WeakReference `, Kotlin's ` WeakReference `) need workarounds.\n2. **Finalizers**: No finalization support. If your objects hold external resources, you need explicit cleanup patterns.\n3. **Interior pointers**: You can't take a reference to a field inside a struct. This limits some optimization patterns.\n4. **Safari support**: WebKit's implementation is still behind flags. Until Safari ships WASM GC unflagged, you need a fallback path for ~20% of web traffic.\n\n## Building for WASM GC Today\n\nFor new projects targeting WebAssembly with a garbage-collected source language, WASM GC is the recommended approach. The toolchain support is there:\n\n- **Kotlin**: ` kotlin-wasm ` target with K2 compiler (stable since Kotlin 2.0)\n- **Dart/Flutter**: ` dart compile wasm ` (stable since Dart 3.3)\n- **OCaml**: ` wasm_of_ocaml ` (experimental but promising)\n- **Java**: GraalWasm with GC support (behind a flag)\n\nFor Rust, C, and C++, WASM GC doesn't change much, you'll keep using linear memory, and that's fine. WASM GC and linear memory coexist in the same module.\n\nThe bottom line: shipping a garbage collector inside your WASM binary was always a hack. WASM GC is the proper solution, and the performance numbers prove it. The era of 3MB Kotlin/WASM bundles is over.\n\n## Frequently asked questions\n\n### What is the WASM GC proposal?\n\nThe WASM GC proposal adds garbage-collected reference types (structs, arrays, and type hierarchies) directly to WebAssembly, letting languages like Kotlin and Dart compile to WASM without shipping their own GC runtime.\n\n### Does WASM GC replace linear memory?\n\nNo. Linear memory still exists for languages like C/C++/Rust. WASM GC adds a parallel heap managed by the host runtime's garbage collector, specifically for languages that need managed memory.\n\n### Which browsers support WASM GC?\n\nChrome 119+ and Firefox 120+ shipped WASM GC support. Safari has partial support behind flags as of early 2026. Check the WebAssembly roadmap for the latest status.\n\n### How much smaller are Kotlin/WASM binaries with GC?\n\nKotlin/WASM GC binaries are typically 60-80% smaller than Kotlin/WASM with a bundled GC, because you no longer ship a custom garbage collector and memory allocator in the binary.\n\n## Tips\n\n1. **Tip:** WASM GC structs are *not* the same as linear memory objects. They live in the host GC heap, so ` struct.new ` allocations are managed by the browser's V8/SpiderMonkey collector, not your own allocator.\n2. **Tip:** When porting Kotlin to WASM GC, avoid boxing primitives manually. The Kotlin/WASM compiler already maps ` Int `, ` Long `, and ` Double ` to WASM GC ` i31ref ` or unboxed fields inside structs.\n3. **Tip:** Use ` ref.cast ` sparingly, each downcast triggers a runtime type check. If your hot path does repeated casts, restructure your type hierarchy to use ` ref.test ` and branch instead.\n4. **Tip:** Profile WASM GC apps with Chrome's ` --experimental-wasm-gc ` flag and the DevTools Memory panel. GC-managed structs show up under *Wasm externals* in heap snapshots.\n\n- webassembly\n- wasm-gc\n- kotlin\n- dart\n- browser-runtimes\n- garbage-collection\n- performance\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"WASM GC: Garbage-Collected Reference Types in WebAssembly","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#title"},{"level":2,"text":"The Problem WASM GC Solves","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#the-problem-wasm-gc-solves"},{"level":2,"text":"What WASM GC Adds","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#what-wasm-gc-adds"},{"level":3,"text":"Struct Types","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#struct-types"},{"level":3,"text":"Array Types","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#array-types"},{"level":3,"text":"Type Hierarchies and Casts","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#type-hierarchies-and-casts"},{"level":2,"text":"Kotlin/WASM: The First Real Production Story","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#kotlinwasm-the-first-real-production-story"},{"level":3,"text":"Binary Size Comparison","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#binary-size-comparison"},{"level":2,"text":"Dart/WASM: Flutter's Web Story Gets Real","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#dartwasm-flutters-web-story-gets-real"},{"level":2,"text":"Performance Analysis: Strengths and Limitations","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#performance-analysis-strengths-and-limitations"},{"level":3,"text":"Where WASM GC Wins","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#where-wasm-gc-wins"},{"level":3,"text":"Where WASM GC Loses","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#where-wasm-gc-loses"},{"level":2,"text":"Cross-Module GC References: The Integration Story","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#cross-module-gc-references-the-integration-story"},{"level":2,"text":"What's Still Missing","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#whats-still-missing"},{"level":2,"text":"Building for WASM GC Today","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#building-for-wasm-gc-today"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#questions"},{"level":3,"text":"What is the WASM GC proposal?","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-1"},{"level":3,"text":"Does WASM GC replace linear memory?","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-2"},{"level":3,"text":"Which browsers support WASM GC?","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-3"},{"level":3,"text":"How much smaller are Kotlin/WASM binaries with GC?","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-4"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#article-tips"}],"sections":[{"heading":"WASM GC: Garbage-Collected Reference Types in WebAssembly","level":1,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes","content":"WebAssembly"},{"heading":"WASM GC: Garbage-Collected Reference Types in WebAssembly","level":1,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#title","content":"2026-01-208 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/wasm-gc-garbage-collection-browser-runtimes>)"},{"heading":"The Problem WASM GC Solves","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#the-problem-wasm-gc-solves","content":"Compiling a garbage-collected language like Kotlin, Dart, or OCaml to WebAssembly has historically been painful. The traditional approach was to compile your language's runtime, including its entire garbage collector, into a WASM module. That GC would then manage objects inside WASM's linear memory, a flat byte array that the host runtime knows nothing about.\n\nThis worked, but it was terrible:\n\n- **Bloated binaries**: You're shipping a full GC implementation (often 200-500KB) in every module.\n- **No integration with the host GC**: The browser's V8 or SpiderMonkey collector has no idea what's alive or dead in your linear memory. Two GCs running independently, completely unaware of each other.\n- **No cycle collection across boundaries**: If your WASM object holds a reference to a JS object and vice versa, neither GC can collect the cycle.\n- **Redundant work**: The browser already has a world-class, highly-optimized garbage collector. Why bring your own?"},{"heading":"What WASM GC Adds","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#what-wasm-gc-adds","content":"The proposal introduces several new type constructs and instructions. The following sections cover the core additions."},{"heading":"Struct Types","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#struct-types","content":"```wasm\n;; Define a struct type for a linked list node\n(type $Node (struct\n (field $value i32)\n (field $next (ref null $Node))\n))\n\n;; Create a new node\n(struct.new $Node\n (i32.const 42) ;; value\n (ref.null $Node) ;; next = null\n)\n\n;; Read the value field\n(struct.get $Node $value) ;; returns i32\n```\n\nThese structs live on the host GC's heap. When nothing references them, the browser's collector reclaims them. No custom allocator, no free lists, no manual memory management."},{"heading":"Array Types","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#array-types","content":"```wasm\n;; A growable array of i32\n(type $IntArray (array (mut i32)))\n\n;; Create an array of 100 elements, all initialized to 0\n(array.new $IntArray\n (i32.const 0) ;; init value\n (i32.const 100) ;; length\n)\n\n;; Get element at index 5\n(array.get $IntArray (i32.const 5))\n\n;; Set element at index 5\n(array.set $IntArray (i32.const 5) (i32.const 999))\n```"},{"heading":"Type Hierarchies and Casts","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#type-hierarchies-and-casts","content":"WASM GC supports structural subtyping:\n\n```wasm\n(type $Shape (struct\n (field $x f64)\n (field $y f64)\n))\n\n(type $Circle (sub $Shape (struct\n (field $x f64)\n (field $y f64)\n (field $radius f64)\n)))\n\n;; Downcast a Shape ref to a Circle ref\n(ref.cast (ref $Circle)\n (local.get $shape_ref)\n)\n\n;; Test if a ref is a Circle (returns i32 0 or 1)\n(ref.test (ref $Circle)\n (local.get $shape_ref)\n)\n```\n\nThe ` i31ref ` type is another gem, it packs a 31-bit integer into a reference, which is perfect for tagged values that languages like OCaml and Scheme rely on."},{"heading":"Kotlin/WASM: The First Real Production Story","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#kotlinwasm-the-first-real-production-story","content":"[JetBrains](<https://www.jetbrains.com/>) has invested heavily in Kotlin/WASM GC, and the results are impressive. The Kotlin/WASM compiler (K2 backend) now targets WASM GC by default, and the difference from the old linear-memory approach is substantial.\n\nThe following Kotlin class and its compiled output illustrate the mapping:\n\n```kotlin\nclass LinkedList<T>(\n val head: T,\n val tail: LinkedList<T>? = null\n) {\n fun length(): Int {\n var count = 0\n var current: LinkedList<T>? = this\n while (current != null) {\n count++\n current = current.tail\n }\n return count\n }\n}\n```\n\nThe Kotlin/WASM compiler maps this to WASM GC structs:\n\n```wasm\n(type $LinkedList (struct\n (field $head (ref $Object))\n (field $tail (ref null $LinkedList))\n (field $vtable (ref $LinkedList_vtable))\n))\n```\n\nNo separate heap. No custom allocator. The browser's GC handles it all."},{"heading":"Binary Size Comparison","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#binary-size-comparison","content":"Testing a medium-complexity Kotlin Compose Multiplatform app (a task manager with about 15 screens):\n\n| Approach | Binary Size | Load Time (3G) | Peak Memory |\n| --- | --- | --- | --- |\n| Kotlin/WASM (old, linear mem + GC) | 2.8 MB | 4.2s | 48 MB |\n| Kotlin/WASM GC | 680 KB | 1.1s | 22 MB |\n| Kotlin/JS | 1.4 MB | 2.3s | 35 MB |\n\nThat's a **75% reduction** in binary size and a **54% reduction** in peak memory. The load time improvement mostly comes from not having to initialize a custom GC at startup."},{"heading":"Dart/WASM: Flutter's Web Story Gets Real","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#dartwasm-flutters-web-story-gets-real","content":"The [Flutter](<https://flutter.dev/>) team has been working on Dart-to-WASM compilation, and WASM GC is the missing piece that makes it viable. Dart 3.3+ compiles to WASM GC, and Flutter Web can now use it in production.\n\n```dart\n// Dart class hierarchy\nabstract class Animal {\n final String name;\n Animal(this.name);\n String speak();\n}\n\nclass Dog extends Animal {\n Dog(String name) : super(name);\n\n @override\n String speak() => '$name says woof!';\n}\n\n// This compiles to WASM GC struct types with subtyping\n// Dog <: Animal, with vtable dispatch for speak()\n```\n\nThe Dart compiler maps class hierarchies directly to WASM GC's structural subtyping. Virtual method dispatch uses ` ref.cast ` and function references, no more vtable trampolines through linear memory.\n\nPerformance numbers from a Flutter Web benchmark (Wonderous app):\n\n| Metric | dart2js | Dart/WASM GC |\n| --- | --- | --- |\n| Frame time (avg) | 18ms | 8ms |\n| Frame time (p99) | 45ms | 14ms |\n| Initial load | 3.1s | 1.8s |\n| Memory (steady state) | 62 MB | 31 MB |\n\nThe p99 frame time improvement is the one that matters most. The long GC pauses that caused poor Flutter Web frame rates are largely eliminated because the browser's incremental collector is doing the work now instead of a stop-the-world collector running inside linear memory."},{"heading":"Performance Analysis: Strengths and Limitations","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#performance-analysis-strengths-and-limitations","content":"Benchmarking WASM GC across different workloads reveals a nuanced picture. It is not universally faster."},{"heading":"Where WASM GC Wins","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#where-wasm-gc-wins","content":"**Object-heavy workloads**, anything that allocates and discards lots of small objects. The browser's generational GC is incredibly optimized for this pattern. A tree-building benchmark:\n\n```javascript\n// Benchmark: build and traverse a binary tree of depth 20\n// 2^20 = ~1M nodes\n\n// WASM GC (Kotlin/WASM): 145ms average, 12ms p99 GC pause\n// Linear memory (C++ custom allocator): 89ms average, 0ms GC pause\n// Linear memory (Kotlin old): 210ms average, 85ms p99 GC pause\n// JavaScript: 320ms average, 25ms p99 GC pause\n```\n\nWASM GC beats the old Kotlin/WASM approach by 30%, and while raw C++ is still faster (no GC overhead at all), the gap is much smaller than you'd expect."},{"heading":"Where WASM GC Loses","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#where-wasm-gc-loses","content":"**Tight numeric loops**, if you're doing matrix math or signal processing, WASM GC structs add indirection that linear memory doesn't have. You're going through reference types instead of reading raw bytes at known offsets.\n\n```wasm\n;; Linear memory: direct load from known offset\n(f64.load (i32.add (local.get $base) (i32.const 16)))\n\n;; WASM GC: field access through reference\n(struct.get $Matrix $m02 (local.get $mat_ref))\n```\n\nIn a matrix multiplication benchmark (1024x1024, f64), the WASM GC version was about 15-20% slower than linear memory. Not catastrophic, but measurable."},{"heading":"Cross-Module GC References: The Integration Story","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#cross-module-gc-references-the-integration-story","content":"The really transformative part is how WASM GC objects integrate with JavaScript. A WASM GC struct reference can be passed directly to JavaScript as an opaque ` WebAssembly.Struct ` object. The browser's GC sees both the JS wrapper and the WASM struct as part of the same object graph.\n\n```javascript\n// JavaScript side\nconst wasmModule = await WebAssembly.instantiateStreaming(\n fetch('app.wasm'),\n imports\n);\n\n// Get a GC-managed struct from WASM\nconst node = wasmModule.exports.createNode(42);\n\n// Store it in a JS data structure\nconst cache = new Map();\ncache.set('node1', node); // JS GC tracks this reference\n\n// When cache is cleared, the WASM struct becomes eligible for collection\n// No leak! The host GC sees the full reference graph.\n```\n\nThis was literally impossible with linear memory. You had to manually pin objects and create weak reference tables to avoid leaks. These leaks in production environments are notoriously among the worst bugs to track down."},{"heading":"What's Still Missing","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#whats-still-missing","content":"WASM GC isn't complete. A few notable gaps remain:\n\n1. **Weak references**: Not in the current spec. Languages that rely on weak refs (Java's ` WeakReference `, Kotlin's ` WeakReference `) need workarounds.\n2. **Finalizers**: No finalization support. If your objects hold external resources, you need explicit cleanup patterns.\n3. **Interior pointers**: You can't take a reference to a field inside a struct. This limits some optimization patterns.\n4. **Safari support**: WebKit's implementation is still behind flags. Until Safari ships WASM GC unflagged, you need a fallback path for ~20% of web traffic."},{"heading":"Building for WASM GC Today","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#building-for-wasm-gc-today","content":"For new projects targeting WebAssembly with a garbage-collected source language, WASM GC is the recommended approach. The toolchain support is there:\n\n- **Kotlin**: ` kotlin-wasm ` target with K2 compiler (stable since Kotlin 2.0)\n- **Dart/Flutter**: ` dart compile wasm ` (stable since Dart 3.3)\n- **OCaml**: ` wasm_of_ocaml ` (experimental but promising)\n- **Java**: GraalWasm with GC support (behind a flag)\n\nFor Rust, C, and C++, WASM GC doesn't change much, you'll keep using linear memory, and that's fine. WASM GC and linear memory coexist in the same module.\n\nThe bottom line: shipping a garbage collector inside your WASM binary was always a hack. WASM GC is the proper solution, and the performance numbers prove it. The era of 3MB Kotlin/WASM bundles is over."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#questions","content":""},{"heading":"What is the WASM GC proposal?","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-1","content":"The WASM GC proposal adds garbage-collected reference types (structs, arrays, and type hierarchies) directly to WebAssembly, letting languages like Kotlin and Dart compile to WASM without shipping their own GC runtime."},{"heading":"Does WASM GC replace linear memory?","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-2","content":"No. Linear memory still exists for languages like C/C++/Rust. WASM GC adds a parallel heap managed by the host runtime's garbage collector, specifically for languages that need managed memory."},{"heading":"Which browsers support WASM GC?","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-3","content":"Chrome 119+ and Firefox 120+ shipped WASM GC support. Safari has partial support behind flags as of early 2026. Check the WebAssembly roadmap for the latest status."},{"heading":"How much smaller are Kotlin/WASM binaries with GC?","level":3,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#question-4","content":"Kotlin/WASM GC binaries are typically 60-80% smaller than Kotlin/WASM with a bundled GC, because you no longer ship a custom garbage collector and memory allocator in the binary."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#article-tips","content":"1. **Tip:** WASM GC structs are *not* the same as linear memory objects. They live in the host GC heap, so ` struct.new ` allocations are managed by the browser's V8/SpiderMonkey collector, not your own allocator.\n2. **Tip:** When porting Kotlin to WASM GC, avoid boxing primitives manually. The Kotlin/WASM compiler already maps ` Int `, ` Long `, and ` Double ` to WASM GC ` i31ref ` or unboxed fields inside structs.\n3. **Tip:** Use ` ref.cast ` sparingly, each downcast triggers a runtime type check. If your hot path does repeated casts, restructure your type hierarchy to use ` ref.test ` and branch instead.\n4. **Tip:** Profile WASM GC apps with Chrome's ` --experimental-wasm-gc ` flag and the DevTools Memory panel. GC-managed structs show up under *Wasm externals* in heap snapshots.\n\n- webassembly\n- wasm-gc\n- kotlin\n- dart\n- browser-runtimes\n- garbage-collection\n- performance\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/wasm-gc-garbage-collection-browser-runtimes"},{"text":"JetBrains","url":"https://www.jetbrains.com/"},{"text":"Flutter","url":"https://flutter.dev/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes"},"headline":"WASM GC: Garbage-Collected Reference Types in WebAssembly","description":"The WASM GC proposal brings struct and array types with automatic garbage collection to WebAssembly. This article examines its impact on Kotlin/WASM, Dart/WASM, and browser runtime architecture.","datePublished":"2026-01-20","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"WebAssembly","keywords":"webassembly, wasm-gc, kotlin, dart, browser-runtimes, garbage-collection, performance","wordCount":1389,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/wasm-gc-garbage-collection-browser-runtimes"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"WASM GC: Garbage-Collected Reference Types in WebAssembly","item":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is the WASM GC proposal?","acceptedAnswer":{"@type":"Answer","text":"The WASM GC proposal adds garbage-collected reference types (structs, arrays, and type hierarchies) directly to WebAssembly, letting languages like Kotlin and Dart compile to WASM without shipping their own GC runtime."}},{"@type":"Question","name":"Does WASM GC replace linear memory?","acceptedAnswer":{"@type":"Answer","text":"No. Linear memory still exists for languages like C/C++/Rust. WASM GC adds a parallel heap managed by the host runtime's garbage collector, specifically for languages that need managed memory."}},{"@type":"Question","name":"Which browsers support WASM GC?","acceptedAnswer":{"@type":"Answer","text":"Chrome 119+ and Firefox 120+ shipped WASM GC support. Safari has partial support behind flags as of early 2026. Check the WebAssembly roadmap for the latest status."}},{"@type":"Question","name":"How much smaller are Kotlin/WASM binaries with GC?","acceptedAnswer":{"@type":"Answer","text":"Kotlin/WASM GC binaries are typically 60-80% smaller than Kotlin/WASM with a bundled GC, because you no longer ship a custom garbage collector and memory allocator in the binary."}}]}]}],"contentHash":"d51ae48791477fbc60ad0ab86ca6a57b65fb0ef58efccdad103a55a1ae39860f","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":66550,"contentBytes":11490}}
