WebAssembly GC Language Support: 5 Core Patterns for Kotlin & Dart Near-Native Browser Execution
Introduction
You're a Kotlin or Dart developer wanting to compile your application to WebAssembly for browser execution, only to discover — GC languages can't compile to Wasm. Traditional Wasm only supports linear memory with no garbage collection mechanism. Kotlin/Dart, being GC languages, must either rely on JS bridging (terrible performance) or bundle their own GC runtime (bloated 30MB+ modules). The experience is nothing short of disastrous.
The WebAssembly GC proposal changes everything. Wasm GC provides native garbage collection primitives for the VM: struct types, array types, and type references. GC languages can directly map to the Wasm GC type system without building their own GC runtime. In 2026, Kotlin/Wasm and Dart/Wasm have moved from experimental to production, achieving near-native execution performance in the browser.
This article dives deep into 5 core patterns, guiding you from zero to building Kotlin/Dart browser applications based on Wasm GC.
Core Concepts Reference
| Concept | Description | Status |
|---|---|---|
| Wasm GC | WebAssembly garbage collection proposal providing struct/array/GC types | Stable |
| Struct Type | GC-managed reference type, similar to OOP objects | Stable |
| Array Type | GC-managed variable-length array supporting reference elements | Stable |
| GC Proposal | Wasm GC Phase 1-3, progressively introducing GC primitives | Phase 3 |
| Kotlin/Wasm | Kotlin backend compiling to Wasm GC, replacing Kotlin/JS | Beta |
| Dart/Wasm | Dart backend compiling to Wasm GC, replacing dart2js | Stable |
| Type Reference | externref/typeref for Wasm-host type interop | Stable |
| Interop | Bridging mechanism between Wasm GC objects and JavaScript objects | Stable |
Problem Analysis: 5 Major Challenges for Wasm GC Languages
1. GC Languages Can't Compile to Wasm
Traditional Wasm only has linear memory and basic value types — no heap, no references, no GC. Kotlin/Dart's object model cannot be directly mapped. Compilers must embed the entire GC runtime into the Wasm module, causing massive bloat and slow startup.
2. Immature Kotlin/Dart Wasm Ecosystem
Before 2024, both Kotlin/Wasm and Dart/Wasm were experimental — lacking IDE support, debugging tools, and third-party library compatibility. Production use was nearly impossible.
3. GC Performance Overhead
Even with native Wasm GC primitives, GC pause times, memory allocation frequency, and generational collection strategies still impact runtime performance, especially in animation-heavy and interaction-intensive scenarios.
4. Complex JS Interop
Wasm GC objects and JavaScript objects belong to different type systems. Cross-boundary passing requires wrapping/unwrapping, and type conversion and lifecycle management are error-prone.
5. Browser Compatibility
Wasm GC requires browser support for new instruction sets. Only Chrome 119+ and Firefox 120+ enable it by default. Safari support came later, and older browsers are completely incompatible.
Pattern 1: Kotlin/Wasm Project Configuration
Kotlin/Wasm supports the Wasm GC target through Kotlin Multiplatform projects:
// build.gradle.kts
kotlin {
wasmJs {
moduleName = "wasmApp"
browser {
commonWebpackConfig {
outputFileName = "wasmApp.js"
}
}
binaries.executable()
}
sourceSets {
val wasmJsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}
}
}
}
HTML Entry File:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Kotlin/Wasm App</title>
</head>
<body>
<script src="wasmApp.js"></script>
</body>
</html>
Key Configuration Points:
- The
wasmJs {}block declares the Wasm GC target; the Kotlin compiler automatically generates GC types binaries.executable()produces a directly runnable Wasm module- Dependencies must support the Wasm target; pure JVM libraries are unavailable
Pattern 2: Dart/Wasm Project Configuration
Dart 3.3+ natively supports the Wasm GC compilation target:
# pubspec.yaml
name: dart_wasm_app
description: Dart Wasm GC application
version: 1.0.0
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
# Compile to Wasm GC
dart compile wasm -O2 -o main.wasm bin/main.dart
# Flutter Web compile to Wasm
flutter build web --wasm
Dart Wasm Entry:
import 'dart:js_interop';
@JS()
extension type JSConsole._(JSObject _) implements JSObject {
external static void log(JSString message);
}
void main() {
final message = 'Hello from Dart/Wasm!'.toJS;
JSConsole.log(message);
}
Key Configuration Points:
dart compile wasmdirectly generates a Wasm GC module without an additional runtimedart:js_interopprovides type-safe JS interop APIs- Flutter Web's
--wasmflag enables the Wasm GC rendering backend
Pattern 3: GC Object and JS Interop
Interop between Wasm GC objects and JS objects is the core challenge. Kotlin and Dart provide different bridging mechanisms:
Kotlin/Wasm Interop:
import kotlinx.js.jsObject
import kotlin.js.JsAny
external interface JsUser : JsAny {
var name: String
var age: Int
}
fun createJsUser(): JsUser = jsObject {
name = "Zhang"
age = 30
}
fun processJsUser(user: JsUser): String {
return "User: ${user.name}, Age: ${user.age}"
}
Dart/Wasm Interop:
import 'dart:js_interop';
@JS()
extension type JsUser._(JSObject _) implements JSObject {
external String get name;
external set name(String value);
external int get age;
external set age(int value);
}
JsUser createJsUser() {
final user = JsUser._(JSObject());
user.name = 'Zhang';
user.age = 30;
return user;
}
Interop Key Points:
- Kotlin uses
JsAnyas the base type for JS objects andjsObject {}to create JS objects - Dart uses
extension type+@JS()annotation for JS type bindings - Note basic type conversions at boundaries: Kotlin
String→JsString, DartString→JSString
Pattern 4: Performance Optimization and Memory Management
Wasm GC application performance optimization requires attention to GC pauses, memory allocation, and object lifecycle:
Kotlin/Wasm Performance Optimization:
// Avoid high-frequency GC: object pool reuse
class ObjectPool<T>(private val factory: () -> T) {
private val pool = mutableListOf<T>()
fun acquire(): T = pool.removeLastOrNull() ?: factory()
fun release(obj: T) {
pool.add(obj)
}
}
data class Particle(var x: Float, var y: Float, var alive: Boolean)
fun simulate() {
val pool = ObjectPool { Particle(0f, 0f, false) }
val particles = mutableListOf<Particle>()
repeat(1000) {
val p = pool.acquire()
p.x = it.toFloat()
p.y = it.toFloat()
p.alive = true
particles.add(p)
}
particles.forEach { p ->
p.alive = false
pool.release(p)
}
particles.clear()
}
Dart/Wasm Performance Optimization:
// Use final and const to reduce GC pressure
class RenderConfig {
final int width;
final int height;
final double scale;
const RenderConfig({
required this.width,
required this.height,
this.scale = 1.0,
});
}
// Avoid frequent closure creation
typedef TransformOp = double Function(double);
double applyTransform(List<double> data, TransformOp op) {
var result = 0.0;
for (final value in data) {
result += op(value);
}
return result;
}
void main() {
final data = List.generate(10000, (i) => i.toDouble());
final op = (double v) => v * 2.0; // Reuse closure
applyTransform(data, op);
}
Performance Optimization Key Points:
- Object pools reduce GC allocation frequency, ideal for animation/game scenarios
const/finalconstructors let the compiler optimize allocation strategies- Avoid creating temporary objects and closures in hot paths
Pattern 5: Production Deployment and Compatibility
Deploying Wasm GC applications requires handling browser compatibility and fallback strategies:
// Kotlin/Wasm: Detect browser support
fun isWasmGcSupported(): Boolean = js(
"() => typeof WebAssembly !== 'undefined' && " +
"WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0]))"
)
fun bootstrap() {
if (isWasmGcSupported()) {
println("Wasm GC supported, loading wasm module...")
startWasmApp()
} else {
println("Wasm GC not supported, falling back to JS...")
startJsApp()
}
}
external fun startWasmApp()
external fun startJsApp()
Dart/Wasm Fallback Configuration:
import 'dart:js_interop';
bool isWasmGcSupported() {
return _checkWasmGcSupport().toDart;
}
@JS('WebAssembly.validate')
external JSBoolean _checkWasmGcSupport(JSUint8Array bytes);
void main() {
if (isWasmGcSupported()) {
runApp(const WasmApp());
} else {
runApp(const JsFallbackApp());
}
}
Nginx Deployment Configuration:
server {
listen 443 ssl;
server_name app.example.com;
# Wasm MIME type
types {
application/wasm wasm;
}
location / {
root /var/www/app;
try_files $uri $uri/ /index.html;
# Wasm caching strategy
location ~* \.wasm$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
}
Pitfall Guide: 5 Common Traps
1. ❌ Building custom GC in Wasm module → ✅ Use Wasm GC native types
Building a custom GC runtime causes 30MB+ module bloat and poor performance. Use Wasm GC struct and array types directly.
2. ❌ Ignoring JsAny/JSObject boundaries → ✅ Use type-safe interop APIs
Directly manipulating JS objects leads to type errors and memory leaks. Use JsAny in Kotlin and extension type in Dart.
3. ❌ Frequent object creation in hot paths → ✅ Object pools and const optimization
Creating objects every frame in animation loops triggers frequent GC pauses. Use object pool reuse or const constructors.
4. ❌ No browser compatibility detection → ✅ Detect and fallback at startup
Wasm GC requires Chrome 119+/Firefox 120+. Loading without detection causes white screens on older browsers.
5. ❌ Mixing Kotlin/JS and Kotlin/Wasm dependencies → ✅ Verify dependency Wasm target support
Not all Kotlin/JS libraries support the Wasm target. Incompatible dependencies cause compilation failures.
Error Troubleshooting: 10 Common Errors
| Error Message | Cause | Solution |
|---|---|---|
Uncaught LinkError: WebAssembly.instantiate() |
Browser doesn't support Wasm GC | Check browser version, provide JS fallback |
TypeError: struct.new requires gc types |
Wasm module uses GC instructions but runtime doesn't support them | Upgrade browser or use polyfill |
CompileError: Wasm GC not enabled |
Wasm GC feature not enabled | Enable #enable-experimental-webassembly-features in Chrome |
Uncaught RuntimeError: illegal cast |
JsAny type cast failure | Check actual JS object type, use safeCast |
OutOfMemoryError |
Excessive GC object allocation | Reduce object creation, use object pools |
LinkError: import alignment mismatch |
Wasm import type doesn't match host | Check JS export function signatures |
TypeError: Cannot read property of undefined |
JS interop accessing undefined property | Use optional chaining ?. |
Compile error: Unresolved reference JsAny |
Missing Wasm JS interop dependency | Add kotlin-js-wasm dependency |
dart2wasm: unsupported import |
Dart library doesn't support Wasm compilation | Check if dependency supports Wasm target |
GC pause > 16ms |
GC pause causing frame drops | Optimize object allocation, reduce GC pressure |
Advanced Optimization Tips
1. Generational GC Tuning
Wasm GC supports generational collection. Reduce old-generation scan frequency to lower pause times. In Kotlin/Dart, avoid cross-generational references to allow short-lived objects to be collected quickly.
2. Wasm GC and Web Workers
Move compute-intensive tasks to Web Workers to prevent GC pauses from blocking the main thread:
// Kotlin/Wasm Worker communication
fun startWorker() {
val worker = js("new Worker('worker.js')")
worker.postMessage(js("{ type: 'compute', data: [1,2,3] }"))
worker.onmessage = { event ->
val result = event.data.result
println("Worker result: $result")
}
}
3. AOT Compilation Optimization
Dart's AOT compiler performs tree-shaking and type specialization in Wasm GC mode, ensuring the final module only contains actually-used code paths.
4. Incremental GC Strategy
For large applications, use Incremental GC to spread GC work across multiple frames, avoiding single long pauses:
// Dart Wasm incremental GC hint
void frameCallback(Duration timestamp) {
performIncrementalGc();
renderFrame();
SchedulerBinding.instance.scheduleFrameCallback(frameCallback);
}
5. Memory Profiling and Monitoring
Use Chrome DevTools' Memory panel to analyze Wasm GC memory allocation, identify GC hotspots and memory leaks.
Comparison: Kotlin/Wasm vs Dart/Wasm vs Blazor WASM vs TeaVM
| Feature | Kotlin/Wasm | Dart/Wasm | Blazor WASM | TeaVM |
|---|---|---|---|---|
| Language | Kotlin | Dart | C# | Java |
| GC Mechanism | Wasm GC native | Wasm GC native | Custom GC runtime | Custom GC runtime |
| Module Size | ~200KB | ~150KB | ~2MB | ~500KB |
| Startup Speed | Fast | Fast | Slow | Medium |
| JS Interop | JsAny API | dart:js_interop | JSInterop | JSBody |
| Framework Support | Compose Multiplatform | Flutter Web | Blazor | None |
| Debugging Support | IDE source maps | DevTools | IDE source maps | Limited |
| Maturity | Beta | Stable | Stable | Experimental |
| Ecosystem Richness | Medium | Rich | Rich | Limited |
Recommended Online Tools
The following tools can significantly boost your efficiency during Wasm GC language development:
- JSON Formatter — Format and validate Wasm module JSON configuration files, debug JS interop data
- Hash Encoding Tool — Generate integrity check hashes for Wasm modules, ensuring distribution security
- cURL to Code Converter — Convert API requests to Kotlin/Dart Wasm code, quickly integrate backend services
Conclusion and Outlook
WebAssembly GC language support in 2026 has evolved from "experimental" to "productive." Kotlin/Wasm and Dart/Wasm directly map to the Wasm GC type system, eliminating the bloat and performance overhead of custom GC runtimes, achieving near-native execution performance in the browser.
"Wasm GC isn't about making GC languages barely run in the browser — it's about making them run natively. When Kotlin and Dart no longer need JS bridging, the last language barrier in web development will be completely broken."
Directions worth watching: Wasm GC FinalizationRegistry, native Wasm GC support for more languages (Java/C#), and deep integration of Wasm GC with the Component Model.
Further Reading
Try these browser-local tools — no sign-up required →