Back to Home

Web Components Issues: Memory and Speed

The article analyzes key shortcomings of Web Components: high memory consumption due to DOM nodes, slow operations compared to JS objects, push-reactivity issues. Comparison with $mol shows 8000x difference at 100k elements.

Web Components: 124 bytes per element and lags in lists
Advertisement 728x90

Why Web Components Fall Short in Performance and Developer Experience

Web components like Lit and Symbiote create each element as an HTMLElement, leading to an allocation of ~124 bytes on the C++ heap. This is 8 times more than the 16 bytes for a JS object in $mol. For 1,000 tasks, Lit/Symbiote consume ~124 KB just for DOM nodes, while $mol uses ~16 KB. With 10,000 tasks, the difference grows to 1.2 MB versus 160 KB.

Without virtualization, an interface with thousands of elements starts to lag. The Custom Elements specification requires inheriting from HTMLElement, which inevitably creates a DOM node even for invisible elements.

// Lit: each todo-item is an HTMLElement (C++ heap, ~124 bytes minimum)
@customElement("todo-item")
export class TodoItem extends LitElement { ... }
// <todo-item> immediately allocates a DOM node on createElement

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

// $mol: a component is a JS object. DOM is created ONLY during rendering.
// 1,000 tasks in the model ≠ 1,000 DOM elements
// $mol renders only visible components (virtualization)
task_rows() {
    return this.task_ids_filtered().map(id => this.Task_row(id))
}

The author of SolidJS confirms: for maximum performance, web components are not suitable because minimizing DOM nodes is impossible.

Google AdInline article slot

Performance Loss Due to DOM Operations

DOM operations are many times slower than JS properties due to C++ bindings and lack of JIT optimization:

| Operation | Time | How Much Worse |

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

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 |

During mass updates (e.g., selecting all tasks in TodoMVC), performance degrades:

| Tasks | Lit | $mol | Difference |

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

| 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: updating via DOM property
render() {
    return html`<label> ${this.text} </label>`;
    // Lit under the hood does: node.textContent = value
}

// $mol: updating via JS property
// this.title() — reading from memo cache (JS heap)
// DOM updates in batches via requestAnimationFrame
task_title(id, next?) {
    return this.task(id, ...)!.title ?? ''
}

HTML parsing in Lit adds overhead.

Differences in Reactivity

Web components use push semantics: events are broadcast to all subscribers on any change.

// Lit: Push via EventTarget + requestUpdate
export class Todos extends EventTarget {
    #notifyChange() {
        this.dispatchEvent(new Event("change")); // push!
    }
    add(text) {
        this.#todos.push({ ... });
        this.#notifyChange(); // ← manual push
    }
}
// Each component subscribes via @updateOnEvent("change")
// On ANY change, ALL subscribers receive a notification

// $mol: Pull — automatic dependency graph
@$mol_mem
groups_completed() {
    // Automatically tracks dependencies:
    // task_ids() → task() → groups
    // Recalculates ONLY when dependencies change
    for (let id of this.task_ids()) {
        var task = this.task(id)!;
        groups[String(task.completed)].push(id);
    }
    return groups;
}
// No need for EventTarget or manual subscriptions

Pull reactivity ($mol, Vue) minimizes unnecessary updates and reduces code volume. Push increases the risk of subscription errors.

Testing and Inheritance

Testing web components requires DOM: creating elements, connectedCallback, waiting for updateComplete. In $mol, tests work with JS objects without DOM.

// $mol: tests WITHOUT 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')
}

Inheritance in Lit requires copying the entire render() block:

class MyTodoApp extends TodoApp {
    render() {
        // Copy 40+ lines of HTML, changing only the footer
    }
}

In $mol, inheritance replaces only the necessary sub-elements:

$my_custom_todo $hyoo_todomvc
    foot <= My_footer $mol_view
// Everything else is inherited

Key Takeaways

  • Web components spend ~124 bytes/element on DOM, which is critical for lists.
  • DOM operations are 30–80 times slower than JS properties, degrading at scale.
  • Push reactivity requires manual subscriptions; pull is simpler and more efficient.
  • Web component tests are slow due to DOM; $mol tests pure objects.
  • Inheritance in web components leads to template copy-pasting.

— Editorial Team

Advertisement 728x90

Read Next