Problemy wydajności i API w web-components
Tworzenie web-componenta z jedną właściwością wymaga znacznej ilości kodu przez Custom Elements API. Podstawowa implementacja ograniczona jest do atrybutów tekstowych, co prowadzi do konieczności serializacji dla innych typów danych.
Przykład minimalnego komponentu:
class MyFoo extends HTMLElement {
_bar = 0
get bar() {
return this._bar
}
set bar( next ) {
this.setAttribute( 'bar', next )
}
static observedAttributes = [ 'bar' ]
attributeChangedCallback( name, prev, next ) {
this[ '_' + name ] = next
}
}
globalThis.customElements.define( 'my-foo', MyFoo )
Aby wspierać liczby, dodaje się serializację JSON z flagą _muted_, aby uniknąć rekursji:
class MyFoo extends HTMLElement {
_bar = 0
get bar() {
return this._bar
}
set bar( next ) {
this._bar = next
this._muted_ = true; try {
this.setAttribute( 'bar', JSON.stringify( next ) )
} finally { this._muted_ = false }
}
_muted_ = false
static observedAttributes = [ 'bar' ]
attributeChangedCallback( name, prev, next ) {
if( this._muted_ ) return
this[ '_' + name ] = JSON.parse( next )
}
}
To prowadzi do problemów z ucieczką znaków w atrybutach i wymaga dodatkowych warstw abstrakcji dla dat, obiektów i innych typów.
Wydajność: mikrosekundy zabijają klatki
Web-components są o rzędy wolniejsze niż zwykłe obiekty JS ze względu na przekazywanie danych przez runtime hosta. Porównanie czasu tworzenia:
- Zwykły obiekt JS: ~0,01 μs
- Web-component: ~1–3 μs
Tysiąc komponentów zajmuje 1–3 ms na utworzenie, co zużywa istotną część 16 ms klatki. Zmiana właściwości również wymaga mikrosekund:
- Proste przypisanie w JS: nanosekundy
- Setter web-componenta: mikrosekundy
Przyczyna — ciągła wymiana danych między JS a C++ runtime przeglądarki, co blokuje JIT-inlining. Pamięć: 124 bajtów na prosty web-component w porównaniu do 16 bajtów dla obiektu JS.
Globalne rejestrowanie i kolizje nazw
Rejestrowanie komponentów jest globalne i nieodwracalne do zamknięcia karty:
customElements.define( 'ya-button', YandexButton )
// ...
customElements.define( 'ya-button', YahooButton ) // 💥 Błąd: nazwa już użyta
Kolizje nazw niszczą modułowość. CustomElementRegistry w Shadow DOM częściowo rozwiązuje ten problem, ale polyfills są niedostępne, a scalanie rejestrow wymaga namespace’u.
Alternatywy:
- Globalny fraktalny rejestr typu DNS
- VerLess — aktualizacja bez naruszania zgodności
Hooki cyklu życia i Shadow DOM
connectedCallback/disconnectedCallback wywołują się przy każdym przeniesieniu komponentu, nawet jeśli pozostaje na tym samym miejscu, powodując kaskadowe zadania asynchroniczne.
Shadow DOM zapewnia izolację, ale utrudnia stylizację: adoptedStyleSheets wymagają dynamicznego importowania stylów z zewnątrz. Nazwy elementów muszą być dokładnie w formacie kebab-case, co koliduje ze standardowymi elementami DOM.
Co ważne
- Wydajność: web-components są 100–1000 razy wolniejsze niż obiekty JS z powodu proxy hosta
- Boilerplate: serializacja/deserializacja jest wymagana dla niestandardowych typów danych
- Rejestracja: globalna, nieodwracalna, z kolizjami nazw
- Cykl życia: hooki wywołują się przy przenoszeniach, powodując kaskady
- Shadow DOM: utrudnia dostosowanie stylów i polyfillowanie
Web-components nie obsługują statycznej typizacji TypeScript ani reaktywności od razu, co wymaga frameworków do aplikacji realnych.
— Editorial Team
Brak komentarzy.