<!-- Canonical: https://lucioduran.com/es/blog/wasm-gc-garbage-collection-browser-runtimes
Language: es
Content-SHA256: a40d01181a0ac3c03f09c6e9f90c8d3d39a987a6e273117e52672bc3c3d03150
-->

WebAssembly

# WASM GC: Tipos con Garbage Collection en WebAssembly

2026-01-209 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/wasm-gc-garbage-collection-browser-runtimes>)

## 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

```wasm
;; 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

```wasm
;; 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:

```wasm
(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](<https://www.jetbrains.com/>) 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:

```kotlin
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:

```wasm
(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](<https://flutter.dev/>) 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.

```dart
// 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:

```javascript
// 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.

```wasm
;; 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.

```javascript
// 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ó.

## Preguntas frecuentes

### ¿Qué es la propuesta WASM GC?

La propuesta WASM GC agrega tipos de referencia con garbage collection (structs, arrays y jerarquías de tipos) directamente a WebAssembly, permitiendo que lenguajes como Kotlin y Dart compilen a WASM sin incluir su propio runtime de GC.

### ¿WASM GC reemplaza la memoria lineal?

No. La memoria lineal sigue existiendo para lenguajes como C/C++/Rust. WASM GC agrega un heap paralelo manejado por el garbage collector del runtime del host, específicamente para lenguajes que necesitan memoria administrada.

### ¿Qué browsers soportan WASM GC?

Chrome 119+ y Firefox 120+ ya incorporaron soporte de WASM GC. Safari tiene soporte parcial detrás de flags a principios de 2026. Verificar el roadmap de WebAssembly para el estado actual.

### ¿Cuánto más pequeños son los binarios de Kotlin/WASM con GC?

Los binarios de Kotlin/WASM GC son típicamente 60-80% más pequeños que Kotlin/WASM con un GC bundleado, porque ya no distribuyes un garbage collector custom y un memory allocator en el binario.

## Tips

1. **Tip:** Los structs de WASM GC *no* son lo mismo que objetos en memoria lineal. Viven en el heap del GC del host, así que las alocaciones con ` struct.new ` las maneja el collector de V8/SpiderMonkey del browser, no tu propio allocator.
2. **Tip:** Al portar Kotlin a WASM GC, no realizar boxing manual de primitivos. El compilador Kotlin/WASM ya mapea ` Int `, ` Long ` y ` Double ` a ` i31ref ` de WASM GC o campos unboxed dentro de structs.
3. **Tip:** Utilizar ` ref.cast ` con moderación, cada downcast dispara un chequeo de tipo en runtime. Si tu hot path hace casts repetidos, reestructurar la jerarquía de tipos para usar ` ref.test ` y branch.
4. **Tip:** Perfilar apps WASM GC con el flag ` --experimental-wasm-gc ` de Chrome y el panel Memory de DevTools. Los structs manejados por el GC aparecen bajo *Wasm externals* en heap snapshots.

- webassembly
- wasm-gc
- kotlin
- dart
- browser-runtimes
- garbage-collection
- rendimiento

[Volver al Blog](<https://lucioduran.com/es/blog>)
