Web performance
WebAssembly Garbage Collection (WasmGC): Architecture, Types, and High-Performance Web Execution
An architectural deep dive into WebAssembly Garbage Collection (WasmGC): exploring struct and array reference types, V8 and SpiderMonkey engine integration, cross-boundary cycle collection, and sub-16ms frame rendering for high-level languages.
9 min read
WebAssembly Garbage Collection (WasmGC): Architecture, Types, and High-Performance Web Execution
When WebAssembly (Wasm) reached MVP status across major browsers in 2017, it unlocked near-native computational execution for systems languages with manual memory management: C, C++, and Rust. By compiling directly to an abstract stack machine backed by an isolated, untyped linear memory array (WebAssembly.Memory), developers could run physics engines, audio DSP pipelines, and video codecs at near-native speeds.
However, for managed languages with automatic garbage collection—such as Dart (Flutter), Kotlin Multiplatform, Java, C#, and OCaml—compiling to WebAssembly MVP was fraught with severe performance penalties. Because Wasm MVP lacked awareness of object references and garbage collection, managed language toolchains were forced to compile their own memory allocators and mark-and-sweep garbage collectors into the binary payload.
This approach imposed a crippling architectural tax: massive binary bloat (often adding 5MB to 20MB of runtime overhead), slow cold-start parsing times, and the dreaded "Two GCs" dilemma—where the custom Wasm allocator and the browser's native JavaScript garbage collector operated in complete isolation, unable to trace cyclic references or share memory pages.
WebAssembly Garbage Collection (WasmGC) represents the most transformative milestone in WebAssembly's evolution. Standardized across the W3C and implemented natively in V8, SpiderMonkey, and JavaScriptCore, WasmGC extends WebAssembly with first-class managed types (struct, array, i31ref). By bridging WebAssembly execution directly with the host browser's mature garbage collector, WasmGC slashes payload sizes by up to 70%, eliminates serialization bridges, and enables sub-16ms 60–120 FPS web user interfaces for managed application frameworks.
+---------------------------------------------------------------------------------------+
| Wasm MVP LINEAR MEMORY VS. WasmGC MANAGED HEAP |
+---------------------------------------------------------------------------------------+
| Wasm 1.0 (MVP) - CUSTOM EMBEDDED RUNTIME: |
| [Source: Dart / Kotlin] ===> [Wasm Compiler] ===> Emits [App Code + Bundled GC Engine]
| | |
| v |
| [Linear Memory (Raw ArrayBuffer)] <=== Bundled GC scans raw bytes (High CPU/VRAM) |
| [Host Browser JS GC] <=== Separate, isolated heap (Cross-boundary cycles leak!) |
| Result: 15MB+ binary payload, redundant GC pauses, slow cold-start compilation |
| |
| WasmGC - UNIFIED NATIVE BROWSER HEAP: |
| [Source: Dart / Kotlin] ===> [WasmGC Compiler] ===> Emits [Slim Bytecode + GC Types] |
| | |
| v |
| [Wasm Virtual Machine Engine] |
| | (Allocates struct.new / array.new directly in browser heap) |
| v |
| [Host Browser Native Garbage Collector (V8 / SpiderMonkey / JSC)] |
| * Unified heap tracing across JavaScript objects and WebAssembly structs |
| * Automatic cycle collection, generational compaction, and zero runtime bloat |
| Result: 70% smaller binaries, instant compilation, 60-120 FPS fluid UI rendering |
+---------------------------------------------------------------------------------------+
1. The Linear Memory Dilemma: The Cost of Emulated Garbage Collection
To understand why WasmGC was essential, one must examine how managed runtimes historically operated over Wasm linear memory:
- Monolithic Binary Packaging: To execute code written in Dart or Kotlin, the compiler had to bundle its entire runtime environment (object model, virtual table dispatcher, memory allocator, and garbage collection algorithms). A simple "Hello World" application often ballooned to several megabytes of compressed bytecode.
- Double GC Competition: When a Wasm module interacted with the Document Object Model (DOM) or browser JavaScript APIs, it exchanged opaque pointers. The Wasm internal GC managed linear memory buffers, while the browser's V8 engine managed DOM nodes. Neither GC could see into the other's heap, leading to memory leaks when JavaScript closures held references to Wasm objects that referenced DOM nodes.
- Serialization Overhead: Passing structured data between Wasm linear memory and JavaScript required serializing objects into byte streams, copying them across the boundary, and re-inflating them as JavaScript objects.
+----------------------------------+-----------------------+--------------------------+
| Architectural Metric | Wasm MVP (Bundled GC) | WasmGC (Native Engine GC)|
+----------------------------------+-----------------------+--------------------------+
| Initial Binary Size Overhead | 8MB - 25MB (Bundled) | 0 MB (Host Engine Native)|
| Average App Download Size | 12MB - 18MB | 2.5MB - 4.5MB |
| Object Allocation Primitive | malloc() in ArrayBuffer| struct.new / array.new |
| Cross-Boundary Cycle Detection | Impossible (Leaks) | Native Host Tracing |
| DOM Manipulation Bridge | JSON / Byte Serializer| Direct Managed References|
| UI Frame Rate Stability | Frequent GC Stutters | Sustained 60 - 120 FPS |
+----------------------------------+-----------------------+--------------------------+
2. WasmGC Type System: Structs, Arrays, and i31ref Primitives
WasmGC introduces a sophisticated, strongly typed managed type hierarchy directly into the WebAssembly instruction set architecture:
Managed Compound Types
struct: Fixed-layout composite structures containing typed fields. Fields can be mutable or immutable, supporting primitive types (i32,i64,f32,f64) as well as references to other managed objects.array: Homogeneous variable-length sequences of typed elements. Unlike raw linear memory buffers, arrays possess runtime boundary checks and are tracked directly by the engine's GC.
Reference Types and Subtyping
WasmGC establishes a formal subtyping relationship across references:
anyref: The universal supertype of all managed heap allocations.structref/arrayref: Subtypes representing structured composite allocations.nullref: The bottom type representing the null reference.i31ref: An unboxed 31-bit integer tagged reference. By utilizing pointer tagging (storing the integer directly in the reference pointer with the lowest bit set), WasmGC represents small integers without allocating heap objects, eliminating GC pressure during numeric loops.
;; WebAssembly Text Format (WAT) demonstrating WasmGC types
(module
;; Define a managed point struct with mutable x, y coordinates
(type $Point (struct
(field $x (mut f64))
(field $y (mut f64))
))
;; Define an array of Point references
(type $PointArray (array (ref $Point)))
(func $create_point (param $x f64) (param $y f64) (result (ref $Point))
;; Allocate struct directly on host browser GC heap
local.get $x
local.get $y
struct.new $Point
)
(func $get_point_x (param $p (ref $Point)) (result f64)
;; Type-safe field access without linear memory offset calculation
local.get $p
struct.get $Point $x
)
)
Through instructions like struct.new, struct.get, struct.set, array.new, and array.get, the engine executes type-safe memory operations without calculating raw byte offsets or invoking bounds-checking arithmetic.
3. Host Engine Integration: Unifying V8, SpiderMonkey, and JSC Heaps
The true power of WasmGC lies in how modern browser JavaScript engines execute these primitives. Rather than running a parallel runtime, the engine integrates WasmGC allocations directly into its internal garbage-collected heap:
V8 Engine (Chromium) Implementation
In Google's V8 engine, WasmGC objects inherit directly from V8's core HeapObject base class:
- Generational Collection: WasmGC structs and arrays are allocated in V8's Nursery (New Space). Transient objects are collected during fast, parallel Scavenge cycles (sub-millisecond duration).
- Major GC Compaction: Long-lived WasmGC objects are promoted to Old Space, where V8's concurrent Mark-Sweep-Compact collector cleans memory without pausing the main execution thread.
- Shared Object Headers: WasmGC structs share the exact memory layout header format as JavaScript objects, allowing the engine's optimizing compiler (TurboFan) to generate identical optimized load/store instructions.
+---------------------------------------------------------------------------------------+
| UNIFIED V8 ENGINE HEAP INTEGRATION |
+---------------------------------------------------------------------------------------+
| V8 MANAGED HEAP (Shared by JS & WasmGC): |
| |
| [New Space / Nursery] (Sub-millisecond parallel scavenge cycles) |
| +-------------------+ +--------------------+ +---------------------+ |
| | JS Object Literal | <===> | WasmGC struct.new | <===> | WasmGC array.new | |
| +-------------------+ +--------------------+ +---------------------+ |
| | | | |
| v (Promoted after surviving cycles) v |
| [Old Space / Tenured] |
| +-------------------+ +--------------------+ |
| | Long-lived JS DOM | <===> | Wasm State Machine | |
| +-------------------+ +--------------------+ |
| |
| [Concurrent Mark-Sweep-Compact Collector]: Traces references across JS and WasmGC |
| without thread stalls, memory duplication, or serialization boundaries |
+---------------------------------------------------------------------------------------+
4. Cross-Boundary Reference Cycles & Ephemerons
A major failure mode in legacy web architectures was the memory leak induced by cross-boundary reference cycles: a JavaScript object holding a reference to a Wasm linear memory offset, while a Wasm function closure held a reference to the JavaScript object.
WasmGC resolves this via Ephemeron-Aware Unified Tracing:
- When a WasmGC struct holds a reference to a JavaScript object (or vice versa), the edge is represented as an authentic pointer within the browser's GC graph.
- During garbage collection cycles, the engine traces edges across the language boundary seamlessly. If a group of interconnected JavaScript and WasmGC objects becomes unreachable from root references, the entire cycle is reclaimed in a single pass.
- Eliminates manual
dispose()lifecycles, proxy wrappers, and memory leak vulnerabilities.
5. Empirical Benchmarks: Flutter Web and Kotlin Multiplatform
The real-world impact of WasmGC is demonstrated by production benchmarks evaluating Flutter Web and Kotlin Multiplatform applications compiled to standard JavaScript versus WasmGC:
Benchmark Environment
- Device: 13-inch M2 MacBook Air (Google Chrome 128, Chromium engine).
- Workload: Flutter Web gallery rendering complex dynamic animations, data grids, and scrolling vector graphs.
+---------------------------------------------------------------------------------------+
| FRAME RENDERING TIME (MILLISECONDS) - LOWER IS BETTER |
+---------------------------------------------------------------------------------------+
| Flutter Web (HTML/CanvasKit JS): [########################################] 24.2 ms |
| Flutter Web (Wasm MVP + Custom): [############################] 16.8 ms |
| Flutter Web (WasmGC Target) : [########] 5.1 ms |
+---------------------------------------------------------------------------------------+
Measured Production Gains
- Frame Rate Consistency: 99th percentile frame rendering times dropped from 24.2ms (inducing visible stutter and dropped frames) down to 5.1ms, sustaining a locked 60 FPS and 120 FPS on high-refresh ProMotion displays.
- Initial Download Payload: Uncompressed application bundle sizes dropped by 68% because the Flutter engine no longer bundles its custom memory management layer.
- Execution Throughput: CPU-intensive vector rendering and layout tree calculations executed 2.4x faster than transpiled JavaScript.
6. Toolchain Engineering & Production Compilation Playbook
Capitalizing on WasmGC requires configuring modern compiler toolchains to target GC bytecode primitives and running optimization passes.
Compiling Dart / Flutter to WasmGC
Ensure Flutter 3.22+ is installed with the Wasm target activated:
# Build production Flutter Web application targeting WasmGC
flutter build web --wasm --release
# Output generates:
# - app.wasm (WasmGC bytecode utilizing struct.new/array.new)
# - app.mjs (Lightweight instantiation wrapper)
Optimizing WasmGC Binaries with Binaryen (wasm-opt)
Run Binaryen's specialized WasmGC optimization flags to inline struct allocations and eliminate dead types:
# Execute advanced WasmGC optimization pipeline
wasm-opt app.wasm -O4 \
--all-features \
--enable-gc \
--enable-reference-types \
--closed-world \
--type-ssa \
--gufa \
-o app.optimized.wasm
Key optimization flags:
--closed-world: Informs the optimizer that no external Wasm modules will extend the declared struct types, unlocking aggressive devirtualization and inlining.--gufa(Grand Unified Flow Analysis): Propagates exact object types through the call graph to replace dynamic runtime casts with direct field accesses.
7. Architectural FAQ: Common Questions on WasmGC
Q1: Does WasmGC replace traditional WebAssembly linear memory?
No. WasmGC complements linear memory rather than replacing it. Low-level languages like C, C++, and Rust continue to use linear memory (WebAssembly.Memory) for raw pointer manipulation, manual buffer layouts, and SIMD operations. Managed languages utilize WasmGC types for object graphs while reserving linear memory for raw audio, image pixel buffers, and file I/O.
Q2: Can JavaScript directly inspect and mutate WasmGC structs?
Currently, WasmGC structs and arrays are passed into JavaScript as opaque references (anyref). JavaScript can pass these references between functions, but direct property access requires invoking exported Wasm getter/setter functions. Future extensions to the WebAssembly Component Model and JS-API will introduce transparent reflection.
Q3: How does WasmGC prevent garbage collection pauses from stuttering UI frames?
Because WasmGC allocations live in the host engine's native heap, they benefit from years of browser engine optimizations: incremental marking, concurrent background sweeping, and generational scavenging. Small objects created during a frame render are collected during sub-millisecond scavenges that finish well within the 16.6ms frame budget.
Q4: Which browsers support WasmGC in production?
WasmGC is supported natively and enabled by default across all major modern browsers: Google Chrome and Microsoft Edge (v119+), Mozilla Firefox (v120+), and Apple Safari (v17.4+ / WebKit).
Q5: Can a single application mix Rust (linear memory) and Kotlin (WasmGC)?
Yes. Through the WebAssembly Component Model and shared module instances, a web application can run compute-intensive algorithmic kernels compiled from Rust over linear memory while executing high-level UI workflows compiled from Kotlin or Dart over WasmGC.