Comment fonctionne réellement le DOM virtuel
« Le DOM virtuel rend React rapide » est une de ces phrases que tout le monde répète sans vraiment pouvoir l'expliquer. Ce n'est pas magique, et ce n'est même pas toujours plus rapide que de toucher directement au vrai DOM : c'est un compromis précis qui résout un problème précis : déterminer l'ensemble minimal de modifications du vrai DOM nécessaires pour passer d'un état d'interface au suivant, sans que vous ayez à le calculer à la main.
Le problème qu'il résout
La manipulation directe du DOM est coûteuse, car le DOM fait beaucoup de travail à chaque changement : recalcul des styles, de la mise en page, et éventuellement un nouveau rendu. Si l'état de votre interface change et que vous ne savez pas exactement ce qui est différent, l'approche naïve consiste à tout démolir et reconstruire :
function render(state) {
container.innerHTML = ''; // wipes everything, loses input focus/scroll position too
container.innerHTML = renderHtml(state);
}
Cela fonctionne, mais c'est du gaspillage et c'est destructeur : les champs de formulaire perdent le focus, la position de défilement se réinitialise, les transitions CSS redémarrent. Ce que l'on veut vraiment, c'est ne modifier que les parties du DOM qui ont changé et laisser tout le reste intact.
Ce qu'est réellement un nœud de DOM virtuel
Un nœud de DOM virtuel est simplement un objet JavaScript ordinaire qui décrit à quoi un élément devrait ressembler, sans aucun appel à l'API du vrai DOM.
// Roughly what React.createElement('button', { className: 'btn' }, 'Save') produces
const vnode = {
type: 'button',
props: { className: 'btn' },
children: ['Save'],
};
Un arbre complet de ces objets est peu coûteux à créer : ce n'est que de l'allocation d'objets, sans moteur de rendu du navigateur. Cette légèreté est tout l'intérêt : on peut construire un arbre entièrement neuf à chaque rendu pour presque rien, contrairement à la reconstruction du vrai DOM.
Le diffing : comparer l'ancien arbre au nouveau
Quand l'état change, le framework construit un nouvel arbre virtuel et le compare au précédent, nœud par nœud. Cette comparaison s'appelle la réconciliation, et une version simplifiée de l'algorithme de base ressemble à ceci :
function diff(oldVNode, newVNode) {
if (oldVNode.type !== newVNode.type) {
return { type: 'REPLACE', newVNode };
}
const propPatches = diffProps(oldVNode.props, newVNode.props);
const childPatches = diffChildren(oldVNode.children, newVNode.children);
return { type: 'UPDATE', propPatches, childPatches };
}
function diffProps(oldProps, newProps) {
const patches = [];
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
patches.push({ key, value: newProps[key] });
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patches.push({ key, value: undefined }); // removed
}
}
return patches;
}
Le résultat n'est pas un nouveau DOM, mais une petite liste de patchs : « cet attribut a changé », « ce nœud de texte a changé », « cet élément a été ajouté ou supprimé ». Seuls ces patchs sont appliqués au vrai DOM, via de vrais appels ciblés à element.setAttribute(), textContent = ou appendChild().
Pourquoi les clés comptent pour les listes
Comparer une liste d'enfants par indice de tableau s'effondre dès que des éléments sont réordonnés, insérés ou retirés au milieu. Sans identité stable par élément, l'algorithme suppose que la position 0 de l'ancien tableau correspond à la position 0 du nouveau, même si l'élément qui s'y trouve a complètement changé.
<!-- Without keys: reordering "B, A, C" makes the diff think every item's
text content changed, when really only the order changed -->
<li>A</li>
<li>B</li>
<li>C</li>
// With a stable key, the algorithm can tell "this specific item moved"
// instead of "everything here is now different"
items.map((item) => ({ type: 'li', key: item.id, children: [item.name] }));
C'est pourquoi chaque API de rendu de liste de chaque framework à DOM virtuel demande une key : ce n'est pas une formalité, c'est l'information que l'algorithme de diffing ne peut littéralement pas déduire seul.
Ce que cela coûte
Construire un arbre virtuel complet et le comparer n'est pas gratuit : pour des interfaces très simples et petites, ou pour des mises à jour dont vous connaissez déjà la forme exacte, la manipulation directe du DOM peut réellement être plus rapide, puisqu'on saute entièrement la construction de l'arbre et la comparaison. Le véritable gain du DOM virtuel n'est pas la vitesse brute à chaque mise à jour : c'est qu'il offre un modèle de programmation déclaratif (« rends cet état sous la forme de cet arbre ») tout en ne touchant au vrai DOM que lorsque c'est nécessaire, sans que vous suiviez manuellement ce qui a changé.
C'est aussi pourquoi les frameworks ont dépassé le « diffing de DOM virtuel » comme unique stratégie. Les compilateurs qui connaissent la structure de votre template à l'avance (Svelte, la compilation de templates plus récente d'Angular) peuvent se passer complètement du diffing à l'exécution et générer des instructions de mise à jour directes à la compilation, car ils n'ont pas besoin de découvrir ce qui a changé : ils le savent déjà grâce au template.
À retenir
Un DOM virtuel est une description sous forme d'objets simples de l'état de l'interface, peu coûteuse à construire et à comparer. Le diffing de deux de ces arbres produit une liste minimale de patchs, et ce sont eux qui touchent réellement au vrai DOM. Il n'est pas intrinsèquement « plus rapide que le DOM » : c'est un moyen d'obtenir un modèle de rendu déclaratif sans payer une reconstruction complète à chaque changement.