Les défis de performance et d'API des composants web
Créer un composant web avec une seule propriété nécessite une quantité importante de code en utilisant l’API Custom Elements. L’implémentation de base ne prend en charge que les attributs de type chaîne, ce qui oblige à sérialiser les autres types de données.
Exemple de composant minimal :
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 )
Pour supporter les nombres, on ajoute une sérialisation JSON avec un indicateur _muted_ pour éviter la récursion :
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 )
}
}
Cela introduit des problèmes d’échappement des chaînes d’attributs et exige des couches d’abstraction supplémentaires pour les dates, les objets et autres types.
Performance : les microsecondes coûtent des frames
Les composants web sont bien plus lents que les objets JS classiques en raison du proxy au runtime. Comparaison du temps de création :
- Objet JS classique : ~0,01 μs
- Composant web : ~1–3 μs
Mille composants prennent entre 1 et 3 ms à être créés — une part significative du budget de 16 ms par frame. Les modifications de propriétés consomment aussi des microsecondes :
- Affectation JS simple : nanosecondes
- Setter de composant web : microsecondes
La cause principale est l’échange constant de données entre JS et le runtime C++ du navigateur, bloquant l’inlineage JIT. En mémoire : 124 octets par composant web trivial contre 16 octets pour un objet JS.
Enregistrement global et conflits de noms
L’enregistrement des composants est global et irréversible tant que l’onglet n’est pas fermé :
customElements.define( 'ya-button', YandexButton )
// ...
customElements.define( 'ya-button', YahooButton ) // 💥 Erreur : nom déjà utilisé
Les conflits de noms rompent la modularité. Bien que CustomElementRegistry dans Shadow DOM aide partiellement, les polyfills ne sont pas disponibles, et fusionner les registres demande une gestion de namespace.
Alternatives :
- Un registre fractal global comme DNS
- VerLess — mise à jour sans rupture de compatibilité
Hooks de cycle de vie et Shadow DOM
connectedCallback/disconnectedCallback se déclenchent à chaque mouvement du composant — même lorsqu’il reste à la même place — provoquant des cascades dans tout l’arbre. Cela entraîne des tâches asynchrones inutiles.
Shadow DOM assure une isolation mais complique le style : adoptedStyleSheets exigent des imports dynamiques de feuilles de style externes. Les noms d’éléments doivent être strictement au format kebab-case, ce qui entre en conflit avec les éléments DOM standards.
Ce qui compte vraiment
- Performance : les composants web sont 100 à 1000 fois plus lents que les objets JS en raison du proxy hôte
- Code boilerplate : la sérialisation/désérialisation est obligatoire pour les propriétés non chaînes
- Enregistrement : global, irréversible, sujet aux conflits de noms
- Cycle de vie : les hooks se déclenchent lors des déplacements, causant des cascades
- Shadow DOM : complexifie la personnalisation des styles et le polyfilling
Les composants web ne supportent pas le typage statique TypeScript ni la réactivité intégrée, rendant indispensable l’utilisation de frameworks pour les applications réelles.
— Editorial Team
Aucun commentaire pour le moment.