Das Morph-Plugin für flackerfreie DOM-Updates bei Livewire und Co.
AI generated
x-data
Alpine
Alpine.js / Morph Plugin
Flackerfreie DOM-Updates
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.

10 Min. Lesezeit @alpinejs/morph Alpine.morph()

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.

11. FAQ: Alpine Morph-Plugin: das Wichtigste auf einen Blick

1Was macht Alpine.morph() anders als ein einfaches innerHTML?
Statt den kompletten alten DOM-Teilbaum zu entfernen und neu aufzubauen, vergleicht Morph alten und neuen HTML-Baum Knoten für Knoten und ändert nur tatsächlich abweichende Attribute und Textinhalte im echten DOM.
2Ist Morph Teil des Alpine-Kerns?
Nein, Morph ist ein separates, offizielles Plugin und muss über @alpinejs/morph installiert und mit Alpine.plugin(morph) vor Alpine.start() registriert werden.
3Warum bleibt der Fokus in einem Eingabefeld nach einem Morph erhalten?
Weil Morph den bestehenden DOM-Knoten wiederverwendet statt ihn zu ersetzen. Der Browser behandelt ihn deshalb als unverändertes Element, Fokus und Cursorposition bleiben an genau diesem Knoten bestehen.
4Wofür wird das key-Attribut bei Morph gebraucht?
Bei Listen, deren Reihenfolge sich ändert, hilft key Morph dabei, ein Element anhand einer stabilen Identität wiederzuerkennen, statt es allein anhand seiner Position zuzuordnen, ähnlich der key-Prop in React.
5Nutzt nur Livewire das Morph-Plugin?
Nein, Livewire ist der bekannteste Einsatzort, weil es Morph intern für jeden Server-Roundtrip verwendet, aber Alpine.morph() lässt sich vollständig eigenständig aufrufen, etwa für eigene Fetch-basierte Teilupdates.
6Bleibt der Zustand einer Alpine-Komponente nach einem Morph erhalten?
Ja, solange Morph das Wurzelelement der Komponente als wiederverwendbaren Knoten erkennt, bleibt der interne x-data-Zustand samt Event-Listenern automatisch bestehen.
7Was passiert, wenn sich der Tag-Name eines Elements komplett ändert?
Dann erkennt Morph keine Wiederverwendbarkeit und tauscht den kompletten Knoten aus, inklusive allem darin enthaltenen Zustand, genau wie bei einem klassischen innerHTML-Austausch.
8Ist Alpine.morph() ein vollständiger Virtual-DOM-Algorithmus wie in React?
Nein, es ist ein bewusst schlankes DOM-zu-DOM-Patching-Verfahren ohne separaten Virtual-DOM-Baum, das direkt zwischen bestehendem und neuem HTML abgleicht.
9Wie lassen sich einzelne Elemente von Morphs automatischem Abgleich ausschließen?
Über die Callback-Optionen updating und removing, die es erlauben, bestimmte Knoten gezielt unangetastet zu lassen, etwa ein von einer dritten Bibliothek verwaltetes Widget.
10Wann lohnt sich x-show statt Morph?
Bei rein clientseitigen Sichtbarkeitswechseln von bereits vorhandenem Markup, ohne dass neues HTML von außen eintrifft, bringt Morph keinen Mehrwert, x-show oder x-if bleiben hier die einfachere Wahl.