Retour à l'accueil

Problèmes Web Components : Mémoire et Vitesse

L'article analyse les principaux défauts des Web Components : consommation de mémoire élevée due aux nœuds DOM, opérations lentes par rapport aux objets JS, problèmes de réactivité push. La comparaison avec $mol montre une différence de 8000x à 100k éléments.

Web Components : 124 octets par élément et ralentissements dans les listes
Advertisement 728x90

Pourquoi les Web Components présentent des lacunes en performance et expérience développeur

Les Web Components comme Lit et Symbiote créent chaque élément en tant que HTMLElement, ce qui entraîne une allocation d'environ 124 octets sur le tas C++. C'est 8 fois plus que les 16 octets pour un objet JS dans $mol. Pour 1 000 tâches, Lit/Symbiote consomment ~124 Ko rien que pour les nœuds DOM, tandis que $mol utilise ~16 Ko. Avec 10 000 tâches, la différence passe à 1,2 Mo contre 160 Ko.

Sans virtualisation, une interface avec des milliers d'éléments commence à ralentir. La spécification Custom Elements exige d'hériter de HTMLElement, ce qui crée inévitablement un nœud DOM même pour les éléments invisibles.

// Lit : chaque todo-item est un HTMLElement (tas C++, ~124 octets minimum)
@customElement("todo-item")
export class TodoItem extends LitElement { ... }
// <todo-item> alloue immédiatement un nœud DOM sur createElement

// Symbiote : de même, chaque <todo-item> = HTMLElement
class TodoItem extends Symbiote { ... }
TodoItem.reg('todo-item');

// $mol : un composant est un objet JS. Le DOM est créé UNIQUEMENT lors du rendu.
// 1 000 tâches dans le modèle ≠ 1 000 éléments DOM
// $mol rend uniquement les composants visibles (virtualisation)
task_rows() {
    return this.task_ids_filtered().map(id => this.Task_row(id))
}

L'auteur de SolidJS confirme : pour une performance maximale, les Web Components ne sont pas adaptés car minimiser les nœuds DOM est impossible.

Google AdInline article slot

Perte de performance due aux opérations DOM

Les opérations DOM sont plusieurs fois plus lentes que les propriétés JS en raison des liaisons C++ et du manque d'optimisation JIT :

| Opération | Temps | Combien de fois pire |

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

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 |

Lors de mises à jour massives (par exemple, sélectionner toutes les tâches dans TodoMVC), la performance se dégrade :

| Tâches | Lit | $mol | Différence |

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

| 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 : mise à jour via propriété DOM
render() {
    return html`<label> ${this.text} </label>`;
    // Lit en interne fait : node.textContent = value
}

// $mol : mise à jour via propriété JS
// this.title() — lecture depuis le cache mémo (tas JS)
// Les mises à jour DOM sont groupées via requestAnimationFrame
task_title(id, next?) {
    return this.task(id, ...)!.title ?? ''
}

L'analyse HTML dans Lit ajoute une surcharge.

Différences en réactivité

Les Web Components utilisent une sémantique push : les événements sont diffusés à tous les abonnés à chaque changement.

// Lit : Push via EventTarget + requestUpdate
export class Todos extends EventTarget {
    #notifyChange() {
        this.dispatchEvent(new Event("change")); // push !
    }
    add(text) {
        this.#todos.push({ ... });
        this.#notifyChange(); // ← push manuel
    }
}
// Chaque composant s'abonne via @updateOnEvent("change")
// À CHAQUE changement, TOUS les abonnés reçoivent une notification

// $mol : Pull — graphe de dépendances automatique
@$mol_mem
groups_completed() {
    // Suit automatiquement les dépendances :
    // task_ids() → task() → groups
    // Recalcule UNIQUEMENT quand les dépendances changent
    for (let id of this.task_ids()) {
        var task = this.task(id)!;
        groups[String(task.completed)].push(id);
    }
    return groups;
}
// Pas besoin de EventTarget ou d'abonnements manuels

La réactivité pull ($mol, Vue) minimise les mises à jour inutiles et réduit le volume de code. Le push augmente le risque d'erreurs d'abonnement.

Tests et héritage

Tester les Web Components nécessite le DOM : créer des éléments, connectedCallback, attendre updateComplete. Dans $mol, les tests fonctionnent avec des objets JS sans DOM.

// $mol : tests SANS 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')
}

L'héritage dans Lit nécessite de copier tout le bloc render() :

class MyTodoApp extends TodoApp {
    render() {
        // Copier 40+ lignes de HTML, en changeant seulement le pied de page
    }
}

Dans $mol, l'héritage remplace uniquement les sous-éléments nécessaires :

$my_custom_todo $hyoo_todomvc
    foot <= My_footer $mol_view
// Tout le reste est hérité

Points clés à retenir

  • Les Web Components dépensent ~124 octets/élément sur le DOM, ce qui est critique pour les listes.
  • Les opérations DOM sont 30 à 80 fois plus lentes que les propriétés JS, se dégradant à grande échelle.
  • La réactivité push nécessite des abonnements manuels ; le pull est plus simple et efficace.
  • Les tests de Web Components sont lents à cause du DOM ; les tests $mol utilisent des objets purs.
  • L'héritage dans les Web Components conduit à du copier-coller de templates.

— Editorial Team

Advertisement 728x90

Lire ensuite