Powrót do strony głównej

Problemy komponentów webowych: pamięć i prędkość

Artykuł analizuje kluczowe wady komponentów webowych: wysokie zużycie pamięci z powodu węzłów DOM, wolne operacje w porównaniu z obiektami JS, problemy reaktywności push. Porównanie z $mol pokazuje różnicę x8000 przy 100k elementów.

Komponenty webowe: 124 bajty na element i lagi w listach
Advertisement 728x90

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.

Google AdInline article slot

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 |

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

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 |

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

Advertisement 728x90

Czytaj dalej