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.
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 |
|----------------------------|------------|----------------|
| obj.title = x (JS) | 1–2 ns | 1x |
| element.textContent = x | 30–60 ns | 30x |
| element.setAttribute | 50–100 ns | 50x |
| 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
No comments yet.