Wie das Virtual DOM wirklich funktioniert
„Das Virtual DOM macht React schnell" ist einer dieser Sätze, die alle wiederholen, ohne ihn wirklich erklären zu können. Es ist keine Magie, und es ist nicht einmal immer schneller, als das echte DOM direkt anzufassen – es ist ein konkreter Kompromiss, der ein konkretes Problem löst: herauszufinden, welche minimale Menge echter DOM-Änderungen nötig ist, um von einem UI-Zustand zum nächsten zu kommen, ohne dass man das von Hand berechnen muss.
Das Problem, das es löst
Direkte DOM-Manipulation ist teuer, weil das DOM pro Änderung viel Arbeit leistet – Stile neu berechnen, Layout, womöglich Neuzeichnen. Ändert sich der UI-Zustand und man weiß nicht genau, was anders ist, ist der naive Ansatz, alles abzureißen und neu aufzubauen:
function render(state) {
container.innerHTML = ''; // wipes everything, loses input focus/scroll position too
container.innerHTML = renderHtml(state);
}
Das funktioniert, ist aber verschwenderisch und destruktiv – Formularfelder verlieren den Fokus, die Scrollposition wird zurückgesetzt, CSS-Transitions starten neu. Was man eigentlich will: nur die Teile des DOM ändern, die sich tatsächlich geändert haben, und alles andere in Ruhe lassen.
Was ein Virtual-DOM-Knoten tatsächlich ist
Ein Virtual-DOM-Knoten ist nur ein einfaches JavaScript-Objekt, das beschreibt, wie ein Element aussehen soll – noch ohne Aufrufe der echten DOM-API.
// Roughly what React.createElement('button', { className: 'btn' }, 'Save') produces
const vnode = {
type: 'button',
props: { className: 'btn' },
children: ['Save'],
};
Ein ganzer Baum solcher Objekte ist billig zu erzeugen – es ist reine Objektallokation, ohne die Rendering-Engine des Browsers. Diese Billigkeit ist der ganze Punkt: Man kann bei jedem Rendern einen komplett neuen Baum bauen, und es kostet fast nichts, anders als das echte DOM neu aufzubauen.
Diffing: alten Baum mit neuem Baum vergleichen
Ändert sich der Zustand, baut das Framework einen neuen virtuellen Baum und vergleicht ihn Knoten für Knoten mit dem vorherigen. Dieser Vergleich heißt Reconciliation, und eine vereinfachte Version des Kernalgorithmus sieht so aus:
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;
}
Das Ergebnis ist kein neues DOM – es ist eine kleine Liste von Patches: „dieses Attribut hat sich geändert", „dieser Textknoten hat sich geändert", „dieses Element wurde hinzugefügt/entfernt". Nur diese Patches werden auf das echte DOM angewendet, über echte, gezielte Aufrufe wie element.setAttribute() / textContent = / appendChild().
Warum Keys bei Listen wichtig sind
Eine Liste von Kindern anhand des Array-Index zu diffen bricht zusammen, sobald Einträge umsortiert, eingefügt oder aus der Mitte entfernt werden. Ohne stabile Identität pro Eintrag nimmt der Algorithmus an, dass Position 0 im alten Array Position 0 im neuen entspricht – selbst wenn sich der Eintrag dort komplett geändert hat.
<!-- 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] }));
Deshalb verlangt jede Listen-Render-API in jedem Virtual-DOM-Framework einen key – er ist keine Formalität, sondern die Information, die der Diffing-Algorithmus buchstäblich nicht selbst ableiten kann.
Was es kostet
Einen vollständigen virtuellen Baum zu bauen und zu diffen ist nicht gratis – bei sehr einfachen, kleinen UIs oder bei Updates, deren genaue Form man bereits kennt, kann direkte DOM-Manipulation tatsächlich schneller sein, weil man den Schritt des Baumaufbaus und Vergleichs ganz überspringt. Der eigentliche Gewinn des Virtual DOM ist nicht rohe Geschwindigkeit bei jedem einzelnen Update; er besteht darin, ein deklaratives Programmiermodell zu bieten („rendere diesen Zustand als diesen Baum"), während trotzdem nur dort das echte DOM berührt wird, wo es nötig ist – ohne dass man selbst verfolgen muss, was sich geändert hat.
Deshalb sind Frameworks auch von „Virtual-DOM-Diffing" als einziger Strategie abgerückt. Compiler, die die Struktur deines Templates im Voraus kennen (Svelte, Angulars neuere Template-Kompilierung), können das Diffing zur Laufzeit ganz überspringen und direkte Update-Anweisungen zur Build-Zeit erzeugen, weil sie nicht herausfinden müssen, was sich geändert hat – sie wissen es bereits aus dem Template.
Fazit
Ein Virtual DOM ist eine Beschreibung des UI-Zustands in einfachen Objekten, billig zu bauen und zu vergleichen. Das Diffing zweier solcher Bäume erzeugt eine minimale Patch-Liste, und die ist es, die das echte DOM tatsächlich berührt. Es ist nicht von Haus aus „schneller als das DOM" – es ist ein Weg zu einem deklarativen Rendering-Modell, ohne bei jeder Änderung einen vollständigen Neuaufbau zu bezahlen.