Dlaczego komponenty webowe ustępują w wydajności i wygodzie programowania
Komponenty webowe, takie jak Lit i Symbiote, tworzą każdy element jako HTMLElement, co prowadzi do alokacji ~124 bajtów na stercie C++. To 8 razy więcej niż 16 bajtów dla obiektu JS w $mol. Dla 1000 zadań Lit/Symbiote zużywają ~124 KB tylko na węzły DOM, $mol — ~16 KB. Przy 10 000 zadań różnica sięga 1,2 MB wobec 160 KB.
Bez wirtualizacji interfejs z tysiącami elementów zaczyna się zacinać. Specyfikacja Custom Elements wymaga dziedziczenia po HTMLElement, co nieuchronnie tworzy węzeł DOM nawet dla niewidocznych elementów.
// Lit: każdy todo-item to HTMLElement (sterta C++, ~124 bajtów minimum)
@customElement("todo-item")
export class TodoItem extends LitElement { ... }
// <todo-item> natychmiast alokuje węzeł DOM przy createElement
// Symbiote: analogicznie, każdy <todo-item> = HTMLElement
class TodoItem extends Symbiote { ... }
TodoItem.reg('todo-item');
// $mol: komponent to obiekt JS. DOM tworzony jest TYLKO przy renderowaniu.
// 1000 zadań w modelu ≠ 1000 elementów DOM
// $mol renderuje tylko widoczne komponenty (wirtualizacja)
task_rows() {
return this.task_ids_filtered().map(id => this.Task_row(id))
}
Autor SolidJS potwierdza: dla maksymalnej wydajności komponenty webowe nie są odpowiednie, ponieważ minimalizacja węzłów DOM jest niemożliwa.
Strata prędkości z powodu operacji DOM
Operacje DOM są wielokrotnie wolniejsze niż właściwości JS z powodu wiązań C++ i braku optymalizacji JIT:
| Operacja | Czas | Ile razy gorzej |
|---------------------------|------------|-----------------|
| 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 |
Przy masowej aktualizacji (np. zaznaczenie wszystkich zadań w TodoMVC) wydajność spada:
| Zadań | Lit | $mol | Różnica |
|---------|-----------|---------|---------|
| 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: aktualizacja przez właściwość DOM
render() {
return html`<label> ${this.text} </label>`;
// Lit pod spodem robi: node.textContent = value
}
// $mol: aktualizacja przez właściwość JS
// this.title() — odczyt z pamięci podręcznej memo (sterta JS)
// DOM aktualizowany zbiorczo przez requestAnimationFrame
task_title(id, next?) {
return this.task(id, ...)!.title ?? ''
}
Parsowanie HTML w Lit dodaje narzut.
Różnice w reaktywności
Komponenty webowe używają semantyki push: zdarzenia rozsyłane są do wszystkich subskrybentów przy każdej zmianie.
// Lit: Push przez EventTarget + requestUpdate
export class Todos extends EventTarget {
#notifyChange() {
this.dispatchEvent(new Event("change")); // push!
}
add(text) {
this.#todos.push({ ... });
this.#notifyChange(); // ← ręczny push
}
}
// Każdy komponent subskrybuje przez @updateOnEvent("change")
// Przy KAŻDEJ zmianie WSZYSCY subskrybenci otrzymują powiadomienie
// $mol: Pull — automatyczny graf zależności
@$mol_mem
groups_completed() {
// Automatycznie śledzi zależności:
// task_ids() → task() → groups
// Przeliczane TYLKO gdy zmieniły się zależności
for (let id of this.task_ids()) {
var task = this.task(id)!;
groups[String(task.completed)].push(id);
}
return groups;
}
// Nie potrzebne ani EventTarget, ani ręczne subskrypcje
Reaktywność pull ($mol, Vue) minimalizuje niepotrzebne aktualizacje i redukuje ilość kodu. Push zwiększa ryzyko błędów w subskrypcjach.
Testowanie i dziedziczenie
Testowanie komponentów webowych wymaga DOM: tworzenie elementów, connectedCallback, oczekiwanie na updateComplete. W $mol testy działają z obiektami JS bez DOM.
// $mol: testy BEZ 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')
}
Dziedziczenie w Lit wymaga kopiowania całego bloku render():
class MyTodoApp extends TodoApp {
render() {
// Kopiujemy 40+ linii HTML, zmieniamy tylko stopkę
}
}
W $mol dziedziczenie zastępuje tylko potrzebne sub-elementy:
$my_custom_todo $hyoo_todomvc
foot <= My_footer $mol_view
// Wszystko inne jest dziedziczone
Co jest ważne
- Komponenty webowe zużywają ~124 bajtów/element na DOM, co jest krytyczne dla list.
- Operacje DOM są 30–80 razy wolniejsze niż właściwości JS, degradacja przy skali.
- Reaktywność push wymaga ręcznych subskrypcji, pull jest prostsza i efektywniejsza.
- Testy komponentów webowych są wolne z powodu DOM, $mol testuje czyste obiekty.
- Dziedziczenie w komponentach webowych prowadzi do kopiowania szablonów.
— Editorial Team
Brak komentarzy.