Volver al inicio

Problemas de Web Components: Memoria y Velocidad

El artículo analiza las principales deficiencias de Web Components: alto consumo de memoria debido a nodos DOM, operaciones lentas comparadas con objetos JS, problemas de reactividad push. Comparación con $mol muestra diferencia de 8000x con 100k elementos.

Web Components: 124 bytes por elemento y retrasos en listas
Advertisement 728x90

Por qué los Web Components se quedan cortos en rendimiento y experiencia de desarrollador

Los web components como Lit y Symbiote crean cada elemento como un HTMLElement, lo que conlleva una asignación de ~124 bytes en el heap de C++. Esto es 8 veces más que los 16 bytes de un objeto JS en $mol. Para 1.000 tareas, Lit/Symbiote consumen ~124 KB solo para nodos DOM, mientras que $mol usa ~16 KB. Con 10.000 tareas, la diferencia aumenta a 1,2 MB frente a 160 KB.

Sin virtualización, una interfaz con miles de elementos empieza a ralentizarse. La especificación de Custom Elements requiere heredar de HTMLElement, lo que inevitablemente crea un nodo DOM incluso para elementos invisibles.

// Lit: cada todo-item es un HTMLElement (heap de C++, ~124 bytes mínimo)
@customElement("todo-item")
export class TodoItem extends LitElement { ... }
// <todo-item> asigna inmediatamente un nodo DOM en createElement

// Symbiote: similarmente, cada <todo-item> = HTMLElement
class TodoItem extends Symbiote { ... }
TodoItem.reg('todo-item');

// $mol: un componente es un objeto JS. El DOM se crea SOLO durante el renderizado.
// 1.000 tareas en el modelo ≠ 1.000 elementos DOM
// $mol renderiza solo los componentes visibles (virtualización)
task_rows() {
    return this.task_ids_filtered().map(id => this.Task_row(id))
}

El autor de SolidJS confirma: para un rendimiento máximo, los web components no son adecuados porque minimizar los nodos DOM es imposible.

Google AdInline article slot

Pérdida de rendimiento por operaciones DOM

Las operaciones DOM son muchas veces más lentas que las propiedades JS debido a los enlaces C++ y la falta de optimización JIT:

| Operación | Tiempo | Cuánto peor |

|----------------------------|------------|-------------|

Google AdInline article slot

| obj.title = x (JS) | 1–2 ns | 1x |

| element.textContent = x | 30–60 ns | 30x |

| element.setAttribute | 50–100 ns | 50x |

Google AdInline article slot

| element.style.color = x | 80–150 ns | 80x |

Durante actualizaciones masivas (ej., seleccionar todas las tareas en TodoMVC), el rendimiento se degrada:

| Tareas | Lit | $mol | Diferencia |

|-----------|-----------|---------|------------|

| 100 | 60 µs | 3 µs | 20x |

| 1.000 | 600 µs | 8 µs | 75x |

| 10.000 | 8 ms | 12 µs | 650x |

| 100.000 | 120 ms | 15 µs | 8000x |

// Lit: actualización vía propiedad DOM
render() {
    return html`<label> ${this.text} </label>`;
    // Lit internamente hace: node.textContent = value
}

// $mol: actualización vía propiedad JS
// this.title() — lectura desde caché memo (heap JS)
// Las actualizaciones DOM se hacen en lotes vía requestAnimationFrame
task_title(id, next?) {
    return this.task(id, ...)!.title ?? ''
}

El análisis HTML en Lit añade sobrecarga.

Diferencias en reactividad

Los web components usan semántica push: los eventos se difunden a todos los suscriptores ante cualquier cambio.

// Lit: Push vía EventTarget + requestUpdate
export class Todos extends EventTarget {
    #notifyChange() {
        this.dispatchEvent(new Event("change")); // push!
    }
    add(text) {
        this.#todos.push({ ... });
        this.#notifyChange(); // ← push manual
    }
}
// Cada componente se suscribe vía @updateOnEvent("change")
// En CUALQUIER cambio, TODOS los suscriptores reciben notificación

// $mol: Pull — grafo de dependencias automático
@$mol_mem
groups_completed() {
    // Rastrea automáticamente dependencias:
    // task_ids() → task() → groups
    // Recalcula SOLO cuando cambian las dependencias
    for (let id of this.task_ids()) {
        var task = this.task(id)!;
        groups[String(task.completed)].push(id);
    }
    return groups;
}
// No necesita EventTarget ni suscripciones manuales

La reactividad pull ($mol, Vue) minimiza actualizaciones innecesarias y reduce el volumen de código. El push aumenta el riesgo de errores de suscripción.

Pruebas y herencia

Probar web components requiere DOM: crear elementos, connectedCallback, esperar updateComplete. En $mol, las pruebas funcionan con objetos JS sin DOM.

// $mol: pruebas SIN DOM
'task add'($) {
    const app = $hyoo_todomvc.make({ $ })
    app.Add().value('test')
    app.Add().submit()
    $mol_assert_equal(app.task_rows().at(-1)!.title(), 'test')
}

La herencia en Lit requiere copiar todo el bloque render():

class MyTodoApp extends TodoApp {
    render() {
        // Copiar 40+ líneas de HTML, cambiando solo el pie
    }
}

En $mol, la herencia reemplaza solo los subelementos necesarios:

$my_custom_todo $hyoo_todomvc
    foot <= My_footer $mol_view
// Todo lo demás se hereda

Conclusiones clave

  • Los web components gastan ~124 bytes/elemento en DOM, lo que es crítico para listas.
  • Las operaciones DOM son 30–80 veces más lentas que las propiedades JS, degradándose a escala.
  • La reactividad push requiere suscripciones manuales; pull es más simple y eficiente.
  • Las pruebas de web components son lentas por el DOM; $mol prueba objetos puros.
  • La herencia en web components lleva a copiar y pegar plantillas.

— Editorial Team

Advertisement 728x90

Leer después