Wie das Alpine-Morph-Plugin bestehende DOM-Knoten intelligent abgleicht
Alpine.morph() ersetzt ein bestehendes DOM-Fragment nicht einfach durch neues HTML, sondern gleicht beide Bäume Knoten für Knoten ab und ändert nur das, was sich tatsächlich unterscheidet. Fokus, Scroll-Position, Eingabewerte und sogar der Zustand einer laufenden Alpine-Komponente bleiben dabei erhalten, ein Grund, warum Livewire das Plugin intern für jeden Server-Roundtrip einsetzt.
Inhaltsverzeichnis
- 1. Das Problem, das Morph löst: naives innerHTML zerstört Zustand
- 2. Installation: @alpinejs/morph als Plugin registrieren
- 3. Grundsyntax: Alpine.morph(el, neuesHtml, options)
- 4. Die key-Funktion: korrektes Zuordnen bei Listen mit Umsortierung
- 5. Warum Fokus und Alpine-Komponentenzustand einen Morph überleben
- 6. Haupteinsatzzweck: wie Livewire Morph für jeden Server-Roundtrip nutzt
- 7. Eigenständige Nutzung außerhalb von Livewire
- 8. Grenzen und Stolperfallen von Morph
- 9. Wann sich Morph lohnt und wann ein einfaches x-show reicht
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem, das Morph löst: naives innerHTML zerstört Zustand
Der einfachste Weg, ein Stück Seite nach einem Server-Request zu aktualisieren, ist container.innerHTML = neuesHtml. Der Browser entfernt dabei den kompletten alten Teilbaum und baut ihn komplett neu auf, selbst wenn sich inhaltlich nur eine einzige Zahl in einem Badge geändert hat. Für den Nutzer bedeutet das: ein gerade fokussiertes Eingabefeld verliert den Fokus, eine laufende CSS-Transition springt zurück auf den Anfang, die Scroll-Position innerhalb eines langen Listen-Containers wird zurückgesetzt, und bereits eingegebener, noch nicht abgeschickter Text in einem Formularfeld ist schlicht weg.
Genau dieses Problem löst Alpine.morph(). Statt den alten Baum wegzuwerfen und einen neuen zu erzeugen, vergleicht Morph den bestehenden DOM-Baum mit dem neu gelieferten HTML-String Knoten für Knoten und ändert im echten DOM ausschließlich das, was sich tatsächlich unterscheidet: ein veränderter Textinhalt, ein geändertes Attribut, ein neu hinzugekommenes oder entferntes Kindelement. Alles andere, inklusive Fokus, Scroll-Position und laufender Animationen, bleibt unangetastet.
2. Installation: @alpinejs/morph als Plugin registrieren
Morph ist kein Teil des Alpine-Kerns, sondern ein offizielles, separates Plugin und muss explizit eingebunden werden. Bei einem Build-Setup mit npm wird das Paket @alpinejs/morph installiert und über Alpine.plugin(morph) registriert, bei einer CDN-Einbindung reicht ein zusätzliches Script-Tag, das vor dem Haupt-Alpine-Script eingebunden wird.
Wichtig ist auch hier wieder der Zeitpunkt: die Registrierung des Plugins muss, genau wie bei eigenen Direktiven und Magic Properties, vor Alpine.start() passieren, sonst steht Alpine.morph() zur Laufzeit schlicht nicht zur Verfügung.
import Alpine from 'alpinejs'
import morph from '@alpinejs/morph'
Alpine.plugin(morph)
Alpine.start()
// CDN-Variante: <script defer src=".../morph.min.js"></script>
// muss VOR dem Haupt-Alpine-Script stehen
3. Grundsyntax: Alpine.morph(el, neuesHtml, options)
Der Aufruf folgt dem Muster Alpine.morph(from, toHtml, options). from ist das bestehende DOM-Element, das aktualisiert werden soll, toHtml entweder ein HTML-String oder bereits ein DOM-Element mit dem gewünschten Zielzustand, und options ein Objekt mit optionalen Callback-Funktionen wie key, updating, updated, adding, added, removing und removed, über die sich der Abgleich feinsteuern lässt.
Intern geht der Algorithmus rekursiv vor: unterscheiden sich Tag-Name oder eine als key erkannte Identität zwischen altem und neuem Knoten, wird der komplette Knoten ausgetauscht. Sind Tag und Identität dagegen gleich, werden nur Attribute und Textinhalt aktualisiert, und der Algorithmus steigt eine Ebene tiefer in die Kindknoten ein, um denselben Abgleich dort fortzusetzen.
let container = document.querySelector('#produkt-liste')
fetch('/produkte?filter=aktiv')
.then(res => res.text())
.then(neuesHtml => {
// Patcht bestehende Knoten, statt sie zu ersetzen
Alpine.morph(container, neuesHtml)
})
4. Die key-Funktion: korrektes Zuordnen bei Listen mit Umsortierung
Bei einer einfachen Liste ohne Umsortierung reicht die reihenfolgebasierte Zuordnung von Morph meistens aus. Sobald sich aber die Reihenfolge von Listeneinträgen ändert, etwa nach einem serverseitigen Sortier- oder Filtervorgang, braucht Morph eine explizite Identität pro Element, um zu erkennen, dass ein Eintrag nur seine Position gewechselt hat, statt gelöscht und neu erstellt worden zu sein. Genau dafür dient das key-Attribut, standardmäßig liest Morph dazu ein einfaches key-HTML-Attribut pro Element aus.
Dieses Konzept ist identisch zur key-Prop in React oder zum :key-Binding in Vues v-for: eine stabile, eindeutige Kennung pro Listenelement, unabhängig von dessen aktueller Position im Baum. Fehlt ein key bei einer Liste, deren Reihenfolge sich ändert, behandelt Morph die Elemente positionsbasiert, was dazu führen kann, dass der falsche DOM-Knoten fälschlich als wiederverwendbar erkannt wird und etwa der Fokus am falschen Listeneintrag landet.
<!-- Vor dem Sortieren -->
<ul id="liste">
<li key="produkt-3">Kamera</li>
<li key="produkt-1">Tastatur</li>
</ul>
<!-- Nach Alpine.morph(liste, neuesHtml) mit umgekehrter Reihenfolge
erkennt Morph anhand von key, dass beide li-Elemente lediglich
die Position gewechselt haben, statt sie neu zu erzeugen -->
<ul id="liste">
<li key="produkt-1">Tastatur</li>
<li key="produkt-3">Kamera</li>
</ul>
5. Warum Fokus und Alpine-Komponentenzustand einen Morph überleben
Der Grund, warum ein Morph so viel mehr Zustand erhält als ein innerHTML-Austausch, liegt in der DOM-Knoten-Identität selbst. Erkennt Morph, dass ein bestehender Knoten wiederverwendet werden kann, bleibt exakt dieses JavaScript-Objekt im Speicher bestehen, nur seine Attribute und sein Textinhalt werden angepasst. Der Browser weiß deshalb nicht, dass dort inhaltlich etwas passiert ist, Fokus, Scroll-Position und laufende CSS-Transitions bleiben an genau diesem unverändert bestehenden Knoten hängen.
Für Alpine selbst gilt dasselbe Prinzip: der reaktive Zustand einer x-data-Komponente wird intern über eine an das jeweilige DOM-Element gebundene Struktur verwaltet, nicht als separates, vom DOM unabhängiges Objekt. Solange Morph das Wurzelelement einer Alpine-Komponente als wiederverwendbaren Knoten erkennt, bleibt dessen kompletter interner Zustand automatisch erhalten, inklusive laufender Event-Listener und bereits gesetzter Watcher, ganz ohne dass die Komponente neu initialisiert werden müsste.
6. Haupteinsatzzweck: wie Livewire Morph für jeden Server-Roundtrip nutzt
Der mit Abstand häufigste praktische Einsatzort von Morph ist Livewire. Nach jeder Server-Interaktion, etwa einem wire:click oder einer wire:model-Änderung, rendert Livewire die betroffene Komponente serverseitig komplett neu zu HTML und schickt dieses Ergebnis an den Client zurück. Statt dieses HTML per innerHTML einzusetzen, was bei jedem Tastendruck in einem gebundenen Eingabefeld zu genau den eingangs beschriebenen Problemen führen würde, nutzt Livewire intern Alpine.morph(), um nur die tatsächlich veränderten Stellen zu patchen.
Für Livewire-Komponenten mit Listen gilt dabei dieselbe key-Logik wie oben beschrieben, dort über das Attribut wire:key statt eines rohen key-Attributs, aber mit identischer Funktion: eine stabile Identität pro Zeile, die auch bei serverseitiger Umsortierung erhalten bleibt.
7. Eigenständige Nutzung außerhalb von Livewire
Morph ist bewusst als eigenständiges, generisches Plugin gebaut und funktioniert vollständig unabhängig von Livewire. Ein naheliegender Anwendungsfall ist ein einfaches, serverseitig gerendertes Teilupdate, ähnlich dem, was htmx anbietet, aber ohne die zusätzliche Bibliothek: ein fetch()-Aufruf liefert ein HTML-Fragment vom Server zurück, und Alpine.morph() patcht den bestehenden Container damit, statt ihn zu ersetzen.
Das lohnt sich überall dort, wo ein bestehender Container periodisch mit frischen Serverdaten aktualisiert werden soll, ohne dass dabei Fokus in einem gleichzeitig offenen Suchfeld oder die Scroll-Position innerhalb der Liste verloren gehen darf, etwa bei einem Live-Dashboard, das sich alle paar Sekunden per Polling selbst aktualisiert.
async function aktualisieren() {
let html = await fetch('/dashboard/fragment').then(r => r.text())
// Erhält Scroll-Position und Fokus im Dashboard-Container
Alpine.morph(document.querySelector('#dashboard'), html)
}
setInterval(aktualisieren, 5000)
8. Grenzen und Stolperfallen von Morph
Morph ist trotz des Namens kein vollständiger Virtual-DOM-Diffing-Algorithmus wie in React oder Vue, sondern ein bewusst schlankes DOM-zu-DOM-Patching-Verfahren. Ändert sich der Tag-Name eines Elements komplett, etwa von div zu section, wird der Knoten trotzdem vollständig ausgetauscht, samt allem darin enthaltenen Zustand. Auch bei sehr großen HTML-Fragmenten mit tausenden Knoten kann der rekursive Abgleich selbst spürbar Zeit kosten, wenn auch in der Regel deutlich weniger als ein kompletter Neuaufbau.
Die Callback-Optionen updating und removing erlauben es, einzelne Knoten gezielt vom automatischen Abgleich auszuschließen, etwa um ein von einer dritten Bibliothek verwaltetes Widget unangetastet zu lassen. Wer diese Callbacks nicht kennt, wundert sich oft, warum ein von außen manipuliertes DOM-Element beim nächsten Morph-Aufruf plötzlich wieder auf seinen serverseitigen Ausgangszustand zurückspringt, denn Morph kennt per Default keine Ausnahmen und gleicht konsequent alles ab, was im gelieferten HTML steht.
9. Wann sich Morph lohnt und wann ein einfaches x-show reicht
Für rein clientseitige Sichtbarkeitswechsel, bei denen ohnehin schon vorhandenes Markup nur ein- und ausgeblendet wird, ist x-show oder x-if weiterhin die richtige, deutlich einfachere Wahl, Morph bringt hier keinen Mehrwert, weil kein neues HTML vom Server kommt. Morph zahlt sich dagegen genau dann aus, wenn tatsächlich neues HTML von außen eintrifft, sei es von Livewire, einem eigenen Fetch-Request oder einem anderen serverseitig gerenderten Fragment, und dabei bestehender clientseitiger Zustand wie Fokus, Scroll-Position oder Formulareingaben erhalten bleiben soll.
Als Faustregel gilt: je häufiger ein Container aktualisiert wird, während der Nutzer aktiv mit ihm interagiert, etwa tippt, scrollt oder eine Animation beobachtet, desto wichtiger wird Morph gegenüber einem einfachen HTML-Austausch. Bei seltenen, vollständigen Seitenwechseln dagegen ist der Unterschied für den Nutzer kaum spürbar, und ein einfacher Austausch bleibt die unkompliziertere Lösung.
| Merkmal | innerHTML ersetzen | Alpine.morph() |
|---|---|---|
| Fokus- und Scroll-Position | Geht verloren | Bleibt erhalten |
| Eingabewerte während des Tippens | Gehen verloren | Bleiben erhalten |
| x-data-Komponentenzustand | Wird neu initialisiert | Bleibt erhalten, solange der Knoten wiederverwendet wird |
| Performance bei kleinen Änderungen | Kompletter Neuaufbau des Teilbaums | Gezieltes Patchen nur der abweichenden Knoten |
| Typischer Einsatz | Einfacher, statischer Austausch ohne aktive Interaktion | Livewire, Polling-Dashboards, Server-Side-Rendering-Updates |
Mironsoft
Alpine.js-Interaktivität für Hyvä-Frontends
Hyvä-Frontend, das mehr Interaktivität braucht, aber ohne React-Overhead?
Wir bauen interaktive Frontend-Komponenten für Hyvä-Themes mit Alpine.js, leichtgewichtig und ohne Build-Step-Komplexität, von einfachen Toggles bis zu komplexen Formular-Flows.
Custom-Komponenten
Interaktive Alpine.js-Komponenten für spezifische Shop-Anforderungen entwickeln.
Performance-Review
Bestehende Alpine.js-Implementierungen auf Reaktivitäts-Fallen und Performance prüfen.
Team-Schulung
Entwickler in Alpine.js-Patterns für Hyvä-Themes praxisnah einarbeiten.
10. Zusammenfassung
Alpine Morph-Plugin: das Wichtigste auf einen Blick
Kernidee
Alpine.morph() gleicht bestehenden DOM-Code mit neuem HTML ab und ändert nur, was sich tatsächlich unterscheidet.
Zustand bleibt
Fokus, Scroll-Position, Eingabewerte und x-data-Zustand überleben ein Morph, solange Knoten wiederverwendet werden.
key-Attribut
Bei Listen mit Umsortierung nötig, damit Morph Elemente korrekt statt positionsbasiert zuordnet.
Nicht nur Livewire
Alpine.morph() funktioniert eigenständig, etwa für eigene Fetch-basierte Teilupdates.