Zurück zur Startseite

Web Components Probleme: Speicher und Geschwindigkeit

Der Artikel analysiert die wichtigsten Mängel von Web Components: hoher Speicherverbrauch durch DOM-Knoten, langsame Operationen im Vergleich zu JS-Objekten, push-Reaktivitätsprobleme. Vergleich mit $mol zeigt 8000x Unterschied bei 100.000 Elementen.

Web Components: 124 Bytes pro Element und Verzögerungen in Listen
Advertisement 728x90

Warum Web Components bei Performance und Developer Experience hinterherhinken

Web Components wie Lit und Symbiote erstellen jedes Element als HTMLElement, was zu einer Allokation von ~124 Bytes auf dem C++-Heap führt. Das ist 8-mal mehr als die 16 Bytes für ein JS-Objekt in $mol. Bei 1.000 Aufgaben verbrauchen Lit/Symbiote ~124 KB nur für DOM-Knoten, während $mol ~16 KB nutzt. Bei 10.000 Aufgaben wächst der Unterschied auf 1,2 MB gegenüber 160 KB.

Ohne Virtualisierung beginnt eine Oberfläche mit Tausenden von Elementen zu ruckeln. Die Custom-Elements-Spezifikation erfordert die Vererbung von HTMLElement, was unweigerlich einen DOM-Knoten erzeugt, selbst für unsichtbare Elemente.

// Lit: Jedes todo-item ist ein HTMLElement (C++-Heap, mindestens ~124 Bytes)
@customElement("todo-item")
export class TodoItem extends LitElement { ... }
// <todo-item> allokiert sofort einen DOM-Knoten bei createElement

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

// $mol: Eine Komponente ist ein JS-Objekt. DOM wird NUR während des Renderings erstellt.
// 1.000 Aufgaben im Modell ≠ 1.000 DOM-Elemente
// $mol rendert nur sichtbare Komponenten (Virtualisierung)
task_rows() {
    return this.task_ids_filtered().map(id => this.Task_row(id))
}

Der Autor von SolidJS bestätigt: Für maximale Performance sind Web Components ungeeignet, da die Minimierung von DOM-Knoten unmöglich ist.

Google AdInline article slot

Performanceverlust durch DOM-Operationen

DOM-Operationen sind um ein Vielfaches langsamer als JS-Eigenschaften aufgrund von C++-Bindings und fehlender JIT-Optimierung:

| Operation | Zeit | Wie viel schlechter |

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

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 |

Bei Massenupdates (z.B. Auswahl aller Aufgaben in TodoMVC) verschlechtert sich die Performance:

| Aufgaben | Lit | $mol | Unterschied |

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

| 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: Update über DOM-Eigenschaft
render() {
    return html`<label> ${this.text} </label>`;
    // Lit macht intern: node.textContent = value
}

// $mol: Update über JS-Eigenschaft
// this.title() — Lesen aus Memo-Cache (JS-Heap)
// DOM-Updates in Batches über requestAnimationFrame
task_title(id, next?) {
    return this.task(id, ...)!.title ?? ''
}

HTML-Parsing in Lit fügt Overhead hinzu.

Unterschiede in der Reaktivität

Web Components nutzen Push-Semantik: Ereignisse werden bei jeder Änderung an alle Abonnenten gesendet.

// Lit: Push über EventTarget + requestUpdate
export class Todos extends EventTarget {
    #notifyChange() {
        this.dispatchEvent(new Event("change")); // Push!
    }
    add(text) {
        this.#todos.push({ ... });
        this.#notifyChange(); // ← manueller Push
    }
}
// Jede Komponente abonniert via @updateOnEvent("change")
// Bei JEDER Änderung erhalten ALLE Abonnenten eine Benachrichtigung

// $mol: Pull — automatischer Abhängigkeitsgraph
@$mol_mem
groups_completed() {
    // Verfolgt automatisch Abhängigkeiten:
    // task_ids() → task() → groups
    // Berechnet NUR neu, wenn Abhängigkeiten sich ändern
    for (let id of this.task_ids()) {
        var task = this.task(id)!;
        groups[String(task.completed)].push(id);
    }
    return groups;
}
// Kein EventTarget oder manuelle Abonnements nötig

Pull-Reaktivität ($mol, Vue) minimiert unnötige Updates und reduziert Codevolumen. Push erhöht das Risiko von Abonnementfehlern.

Testing und Vererbung

Das Testen von Web Components erfordert DOM: Elemente erstellen, connectedCallback, Warten auf updateComplete. In $mol funktionieren Tests mit JS-Objekten ohne DOM.

// $mol: Tests OHNE 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')
}

Vererbung in Lit erfordert das Kopieren des gesamten render()-Blocks:

class MyTodoApp extends TodoApp {
    render() {
        // 40+ Zeilen HTML kopieren, nur die Fußzeile ändern
    }
}

In $mol ersetzt Vererbung nur die notwendigen Sub-Elemente:

$my_custom_todo $hyoo_todomvc
    foot <= My_footer $mol_view
// Alles andere wird vererbt

Wichtige Erkenntnisse

  • Web Components verbrauchen ~124 Bytes/Element für DOM, was für Listen kritisch ist.
  • DOM-Operationen sind 30–80-mal langsamer als JS-Eigenschaften, verschlechtern sich bei Skalierung.
  • Push-Reaktivität erfordert manuelle Abonnements; Pull ist einfacher und effizienter.
  • Web-Component-Tests sind langsam wegen DOM; $mol testet reine Objekte.
  • Vererbung in Web Components führt zu Template-Copy-Pasting.

— Editorial Team

Advertisement 728x90

Weiterlesen