El Problema Que Resuelve WASM GC

Compilar un lenguaje con garbage collection como Kotlin, Dart u OCaml a WebAssembly ha sido históricamente doloroso. El approach tradicional era compilar el runtime de tu lenguaje, incluyendo todo su garbage collector, dentro de un módulo WASM. Ese GC después manejaba objetos dentro de la memoria lineal de WASM, un array plano de bytes del cual el runtime del host no sabe absolutamente nada.

Esto funcionaba, pero era horrible:

  • Binarios inflados: Estás distribuyendo una implementación completa de GC (generalmente 200-500KB) en cada módulo.
  • Sin integración con el GC del host: El collector de V8 o SpiderMonkey del browser no tiene idea qué está vivo o muerto en tu memoria lineal. Dos GCs corriendo de forma independiente, completamente ajenos uno del otro.
  • Sin recolección de ciclos entre boundaries: Si tu objeto WASM tiene una referencia a un objeto JS y viceversa, ningún GC puede recolectar el ciclo.
  • Trabajo redundante: El browser ya tiene un garbage collector de primera línea, ultra-optimizado. ¿Para qué traer el tuyo?

Qué Agrega WASM GC

La propuesta introduce varias construcciones de tipos e instrucciones nuevas. A continuación se presenta el núcleo:

Tipos Struct

;; Definir un tipo struct para un nodo de lista enlazada
(type $Node (struct
 (field $value i32)
 (field $next (ref null $Node))
))

;; Crear un nuevo nodo
(struct.new $Node
 (i32.const 42) ;; value
 (ref.null $Node) ;; next = null
)

;; Leer el campo value
(struct.get $Node $value) ;; retorna i32

Estos structs viven en el heap del GC del host. Cuando nada los referencia, el collector del browser los reclama. Sin allocator custom, sin free lists, sin manejo manual de memoria.

Tipos Array

;; Un array growable de i32
(type $IntArray (array (mut i32)))

;; Crear un array de 100 elementos, todos inicializados en 0
(array.new $IntArray
 (i32.const 0) ;; valor init
 (i32.const 100) ;; longitud
)

;; Obtener elemento en índice 5
(array.get $IntArray (i32.const 5))

;; Setear elemento en índice 5
(array.set $IntArray (i32.const 5) (i32.const 999))

Jerarquías de Tipos y Casts

WASM GC soporta subtipado estructural: WASM GC soporta subtipado estructural:

(type $Shape (struct
 (field $x f64)
 (field $y f64)
))

(type $Circle (sub $Shape (struct
 (field $x f64)
 (field $y f64)
 (field $radius f64)
)))

;; Downcast de una ref Shape a una ref Circle
(ref.cast (ref $Circle)
 (local.get $shape_ref)
)

;; Testear si una ref es un Circle (retorna i32 0 o 1)
(ref.test (ref $Circle)
 (local.get $shape_ref)
)

El tipo i31ref es otro componente notable, empaqueta un entero de 31 bits en una referencia, que es perfecto para valores taggeados que lenguajes como OCaml y Scheme usan todo el tiempo.

Kotlin/WASM: La Primera Historia Real de Producción

JetBrains ha invertido intensamente a Kotlin/WASM GC, y los resultados son genuinamente impresionantes. El compilador Kotlin/WASM (backend K2) ahora apunta a WASM GC por defecto, y la diferencia con el viejo approach de memoria lineal es abismal.

A continuación se presenta una clase Kotlin y a qué compila por debajo:

class LinkedList<T>(
 val head: T,
 val tail: LinkedList<T>? = null
) {
 fun length(): Int {
 var count = 0
 var current: LinkedList<T>? = this
 while (current != null) {
 count++
 current = current.tail
 }
 return count
 }
}

El compilador Kotlin/WASM mapea esto a structs de WASM GC:

(type $LinkedList (struct
 (field $head (ref $Object))
 (field $tail (ref null $LinkedList))
 (field $vtable (ref $LinkedList_vtable))
))

Sin heap separado. Sin allocator custom. El GC del browser maneja todo.

Comparación de Tamaño de Binarios

Al testear una app Kotlin Compose Multiplatform de complejidad media (un task manager con unas 15 pantallas):

Approach Tamaño Binario Tiempo de Carga (3G) Memoria Pico
Kotlin/WASM (viejo, mem lineal + GC) 2.8 MB 4.2s 48 MB
Kotlin/WASM GC 680 KB 1.1s 22 MB
Kotlin/JS 1.4 MB 2.3s 35 MB

Eso es una reducción del 75% en tamaño de binario y una reducción del 54% en memoria pico. La mejora en tiempo de carga viene mayormente de no tener que inicializar un GC custom al arranque.

Dart/WASM: La Historia Web de Flutter Se Pone Seria

El equipo de Flutter viene trabajando en compilación de Dart a WASM desde hace tiempo, y WASM GC es la pieza que faltaba para hacerlo viable. Dart 3.3+ compila a WASM GC, y Flutter Web ya puede usarlo en producción.

// Jerarquía de clases en Dart
abstract class Animal {
 final String name;
 Animal(this.name);
 String speak();
}

class Dog extends Animal {
 Dog(String name) : super(name);

 @override
 String speak() => '$name says woof!';
}

// Esto compila a tipos struct de WASM GC con subtipado
// Dog <: Animal, con vtable dispatch para speak()

El compilador de Dart mapea jerarquías de clases directamente al subtipado estructural de WASM GC. El dispatch de métodos virtuales usa ref.cast y function references, no más trampolines de vtable a través de memoria lineal.

Números de rendimiento de un benchmark de Flutter Web (app Wonderous):

Métrica dart2js Dart/WASM GC
Frame time (avg) 18ms 8ms
Frame time (p99) 45ms 14ms
Carga inicial 3.1s 1.8s
Memoria (estado estable) 62 MB 31 MB

La mejora en frame time p99 es la que más importa. Esas pausas largas de GC que hacían que Flutter Web se sintiera con latencia perceptible? Se fueron casi todas porque el collector incremental del browser está haciendo el trabajo ahora, en vez de un collector stop-the-world corriendo dentro de memoria lineal.

Análisis de Rendimiento: Fortalezas y Limitaciones

Los benchmarks de WASM GC en distintos workloads revelan un panorama con matices. No es universalmente más rápido.

Dónde Gana WASM GC

Workloads pesados en objetos, cualquier cosa que aloca y descarta muchos objetos chicos. El GC generacional del browser está increíblemente optimizado para este patrón. Un benchmark de construcción de árboles:

// Benchmark: construir y recorrer un árbol binario de profundidad 20
// 2^20 = ~1M nodos

// WASM GC (Kotlin/WASM): 145ms promedio, 12ms pausa GC p99
// Memoria lineal (C++ allocator custom): 89ms promedio, 0ms pausa GC
// Memoria lineal (Kotlin viejo): 210ms promedio, 85ms pausa GC p99
// JavaScript: 320ms promedio, 25ms pausa GC p99

WASM GC supera al viejo approach Kotlin/WASM por 30%, y si bien C++ crudo sigue siendo más rápido (cero overhead de GC), la brecha es mucho más pequeña de lo que se esperaría.

Dónde Pierde WASM GC

Loops numéricos tight, si se está haciendo matemática de matrices o procesamiento de señales, los structs de WASM GC agregan indirección que la memoria lineal no tiene. Estás pasando por tipos de referencia en vez de leer bytes crudos en offsets conocidos.

;; Memoria lineal: load directo desde offset conocido
(f64.load (i32.add (local.get $base) (i32.const 16)))

;; WASM GC: acceso a campo a través de referencia
(struct.get $Matrix $m02 (local.get $mat_ref))

En un benchmark de multiplicación de matrices (1024x1024, f64), la versión WASM GC fue alrededor de 15-20% más lenta que memoria lineal. No es catastrófico, pero es medible.

Referencias GC Cross-Module: La Historia de Integración

La parte realmente transformadora es cómo los objetos WASM GC se integran con JavaScript. Una referencia a struct de WASM GC se puede pasar directamente a JavaScript como un objeto opaco WebAssembly.Struct. El GC del browser ve tanto el wrapper JS como el struct WASM como parte del mismo grafo de objetos.

// Lado JavaScript
const wasmModule = await WebAssembly.instantiateStreaming(
 fetch('app.wasm'),
 imports
);

// Obtener un struct manejado por GC desde WASM
const node = wasmModule.exports.createNode(42);

// Guardarlo en una estructura de datos JS
const cache = new Map();
cache.set('node1', node); // El GC de JS trackea esta referencia

// Cuando se limpia el cache, el struct WASM queda elegible para recolección
// ¡Sin leak! El GC del host ve el grafo de referencias completo.

Esto era literalmente imposible con memoria lineal. Tenías que fijar objetos manualmente y crear tablas de weak references para evitar leaks. Estos leaks en producción son notoriamente de los peores bugs para rastrear.

Qué Falta Todavía

WASM GC no está completo. Algunos gaps notables que quedan:

  1. Weak references: No están en la spec actual. Lenguajes que dependen de weak refs (el WeakReference de Java, el WeakReference de Kotlin) necesitan workarounds.
  2. Finalizadores: Sin soporte de finalización. Si tus objetos tienen recurse es externos, se necesita patrones de cleanup explícito.
  3. Interior pointers: No se puede tomar una referencia a un campo dentro de un struct. Esto limita algunos patrones de optimización.
  4. Soporte de Safari: La implementación de WebKit sigue detrás de flags. Hasta que Safari incorpore WASM GC sin flag, se necesita un fallback path para ~20% del tráfico web.

Construyendo para WASM GC Hoy

Para proyectos nuevos que apuntan a WebAssembly con un lenguaje fuente con garbage collection, WASM GC es el approach recomendado. El soporte de toolchain está:

  • Kotlin: target kotlin-wasm con compilador K2 (stable desde Kotlin 2.0)
  • Dart/Flutter: dart compile wasm (stable desde Dart 3.3)
  • OCaml: wasm_of_ocaml (experimental pero prometedor)
  • Java: GraalWasm con soporte GC (detrás de un flag)

Para Rust, C y C++, WASM GC no cambia mucho, se va a seguir usando memoria lineal, y está perfecto. WASM GC y memoria lineal coexisten en el mismo módulo.

La línea de fondo: distribuir un garbage collector dentro de tu binario WASM siempre fue un hack. WASM GC es la solución correcta, y los números de rendimiento lo prueban. La era de los bundles Kotlin/WASM de 3MB terminó.