loke.dev
Header image for Is WasmGC the Secret to Shipping Non-JavaScript Frameworks Without the Performance Tax?

Is WasmGC the Secret to Shipping Non-JavaScript Frameworks Without the Performance Tax?

Explore how the arrival of native Garbage Collection for WebAssembly removes the final barrier for languages like Kotlin, Dart, and Java to run at native speeds in the browser.

· 5 min read

Have you ever wondered why, despite WebAssembly being "the future" for years, we’re still mostly stuck writing JavaScript for every single web app that isn't a heavy-duty video editor or a 3D game?

The dream was simple: write in any language, compile to Wasm, and run at near-native speeds. But if you've ever tried to ship a "Hello World" in C# or Kotlin to the browser via Wasm, you probably noticed a massive problem. Your tiny app came with a 5MB to 10MB "runtime tax." Why? Because those languages expect a Garbage Collector (GC) to be there to clean up their messes, and until recently, Wasm didn't have one.

You were essentially shipping an entire virtual machine inside your binary just to manage memory. That’s not a "portable format"; that’s a hostage situation for your user’s bandwidth.

The "Double GC" Nightmare

Before WasmGC, if you wanted to run a managed language like Dart or Java, you had two choices, and both were bad:

1. Bring Your Own GC: You compile a garbage collector into your Wasm binary. This makes your file sizes huge and your performance unpredictable because you have a GC running inside a sandbox that is *already* being monitored by the browser's engine.
2. Linear Memory Gymnastics: You manually manage memory in a giant "Linear Memory" array. This is fast, but it makes interacting with the DOM (which is managed by the browser's GC) a total nightmare of pointers and "glue code" that breaks if you look at it funny.

WasmGC changes the game by letting Wasm talk directly to the browser’s built-in garbage collector. Instead of shipping a GC, you just tell the browser: "Hey, I've got some structured data here. You track it."

What WasmGC actually looks like

In the old days, Wasm only understood numbers (i32, f64, etc.). With WasmGC, we get structured types. We can define structs and arrays that the browser's engine understands.

Here is a snippet of what the WebAssembly Text format (WAT) looks like with these new types:

(module
  ;; Define a 'Point' struct with two 32-bit integers
  (type $Point (struct (field i32) (field i32)))

  (func $create_point (result (ref $Point))
    ;; Allocate the struct on the GC heap
    (struct.new $Point (i32.const 10) (i32.const 20))
  )

  (export "create_point" (func $create_point))
)

In this example, the struct.new instruction tells the browser to allocate memory for a Point. If that point is no longer reachable, the browser’s GC—the same one that cleans up your JavaScript objects—will reclaim that memory. No manual free() calls, no 2MB runtime included in your build.

Why this is a massive win for Kotlin and Dart

If you're a Flutter developer, WasmGC is the difference between "Flutter Web is a cool experiment" and "Flutter Web is a production-ready beast."

Previously, Flutter for Web had to use a complex bridge to handle memory. Now, the Dart compiler can map Dart classes directly to WasmGC structs. I’ve seen experimental builds where the initial payload size dropped by 50-60% just by switching to WasmGC targets.

Take a look at how a language like Kotlin might represent a simple class when targeting WasmGC:

// Your Kotlin code
class User(val id: Int, val name: String)

fun main() {
    val u = User(1, "Alice")
    println(u.name)
}

When compiled, the Kotlin/Wasm compiler translates that User class into a Wasm struct. When the main function finishes and u goes out of scope, the browser's engine sees it as a reference with zero owners and wipes it. It's seamless.

The "Performance Tax" isn't just about speed

We often talk about "performance" as "how fast can I crunch numbers?" But on the web, performance is mostly Time to Interactive (TTI).

If your Wasm module is 10MB because it includes the Java Virtual Machine's memory management logic, your TTI is garbage. Your user has already closed the tab by the time the GC is initialized. WasmGC allows for "Lean Wasm."

Code Example: Accessing WasmGC from JS

While WasmGC is about the internal memory of the Wasm module, you still need to talk to it from JavaScript. Here’s how you might interact with a WasmGC-enabled module:

const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('my_gc_app.wasm'), 
  { /* imports */ }
);

// Suppose this returns a reference to a WasmGC struct
const point = wasmModule.instance.exports.create_point();

// In modern browsers, you can pass these references back and forth
// without the expensive "serialization" overhead.
console.log(point); 

The Gotchas (Because there’s always a catch)

It’s not all sunshine and rainbows. I’ve run into a few hurdles while playing with this:

1. Browser Support: WasmGC is shipped in Chrome and Firefox, but Safari is (as usual) taking its time. If you need to support older browsers, you’re stuck with the old "Fat Binary" approach or a polyfill that kills performance.
2. Tooling Maturity: While the compilers for Kotlin and Dart are moving fast, the debugging tools are still a bit... let's call them "minimalist." Inspecting a WasmGC struct in the DevTools isn't as nice as inspecting a standard JS object yet.
3. The DOM Bridge: Even with WasmGC, you still can't *directly* manipulate the DOM from Wasm. You still have to call out to JavaScript. WasmGC makes passing the data for those calls much faster, but it doesn't eliminate the "JS Glue" entirely.

Is it the secret sauce?

I’d argue yes. WasmGC is the final piece of the puzzle that makes non-JS frameworks feel "native" to the web.

We are moving away from an era where WebAssembly was just for "math-heavy" tasks and into an era where you can reasonably choose a language based on its ecosystem and developer experience, rather than its proximity to the JavaScript runtime.

If you’ve been ignoring WebAssembly because you didn't want to deal with manual memory management or massive binaries, it’s time to take another look at the Kotlin or Dart Wasm targets. The tax has finally been repealed.