Support-Widget in Single-Page-Apps einbinden: Routenwechsel, Shadow DOM, Ladezeit
Was ein Support-Widget in einer Single-Page-App leisten muss: Routenwechsel überstehen, Stile isolieren, die Seite nicht bremsen. Mit den technischen Gründen dahinter.
Martin Semmele

Inhalt
- 01Warum ein Fremdskript in einer Single-Page-App anders lebt
- 02Routenwechsel: Das Widget gehört nicht in den Anwendungsbaum
- 03Shadow DOM: Was die Stiltrennung leistet und was nicht
- 04Ladezeit: Eine Zeile entscheidet
- 05Einbindung über einen Tag-Manager
- 06Einwilligung und das Laden nach Klick
- 07Die Prüfliste nach dem Einbau
- 08Häufig gestellte Fragen
Wichtige Erkenntnisse
- 92 Prozent aller Webseiten laden mindestens eine Fremdressource; ein Support-Widget ist eine davon und muss sich entsprechend benehmen.
- Ein Widget, das einmal am Dokument montiert wird und nicht im Anwendungsbaum lebt, überlebt jeden clientseitigen Routenwechsel ohne Zutun.
- Shadow DOM trennt Stile in beide Richtungen, aber nicht Tastaturfokus, Schriften oder die Stapelreihenfolge gegenüber einem Cookie-Banner.
- Ein Skript mit dem Attribut async blockiert das Parsen der Seite nicht; das ist die eine Zeile, die über die Ladezeit entscheidet.
- Doppelmontage, verlorene Gesprächskennungen und ein nachgeladenes Schriftpaket sind die drei Fehler, die in der Praxis am häufigsten auftreten.
Warum ein Fremdskript in einer Single-Page-App anders lebt
Auf einer klassischen Website wird bei jedem Klick eine neue Seite geladen. Ein Skript, das am Ende des Dokuments steht, läuft damit bei jedem Seitenaufruf neu, und alles, was es sich merken will, muss es irgendwo ablegen. In einer Single-Page-App ist es umgekehrt: Das Dokument wird genau einmal geladen, danach tauscht das Framework nur noch Teile des Baums aus. Ein Skript, das einmal gelaufen ist, läuft nicht wieder.
Das ist für ein Support-Widget zunächst eine gute Nachricht. Es muss sich nicht bei jedem Routenwechsel neu aufbauen, und das offene Gespräch bleibt einfach offen. Die schlechte Nachricht folgt aus demselben Umstand: Alles, was das Widget falsch macht, bleibt ebenfalls bestehen, bis jemand die Seite hart neu lädt. Ein Stilkonflikt, ein zweiter Launcher unten rechts, ein Fokus, der im Widget hängen bleibt – nichts davon räumt ein Routenwechsel auf.
Laut dem Web Almanac 2024 laden 92 Prozent der untersuchten Seiten mindestens eine Fremdressource, und bei den tausend größten Seiten liegt der Median bei 66 verschiedenen Drittanbietern1. Dein Support-Widget ist einer davon. Es konkurriert mit Analyse-Skripten, Cookie-Bannern und Schriftpaketen um Ladezeit, Stapelreihenfolge und Tastaturfokus. Die drei folgenden Abschnitte nehmen die drei Konflikte einzeln auseinander.
Routenwechsel: Das Widget gehört nicht in den Anwendungsbaum
Der häufigste Fehler beim Einbau in React, Vue oder Angular ist gut gemeint: Das Widget wird als Komponente in die Anwendung geholt, damit es „sauber“ im Baum steht. Damit hängt es an der Lebensdauer dieser Komponente. Wird die Route gewechselt und das Layout neu gerendert, wird das Widget abgebaut und neu montiert. Der sichtbare Effekt: Das Chatfenster klappt zu, der Verlauf ist weg oder erscheint doppelt, und je nach Implementierung wird ein zweites Gespräch eröffnet.
Einmal am Dokument montieren, nicht in einer Komponente.
Die robuste Variante ist die einfachste: Das Skript wird einmal geladen, hängt seinen Container direkt an das body-Element und lebt damit außerhalb von allem, was das Framework verwaltet. Ein Routenwechsel tauscht Knoten innerhalb des Anwendungs-Containers aus; der Widget-Container daneben bleibt unberührt. Genau so arbeitet das Comlayer-Widget: Es liest sein eigenes Script-Tag über document.currentScript aus, erzeugt ein einzelnes Host-Element am body und prüft vor der Montage, ob dieses Element schon existiert. Wird das Skript ein zweites Mal ausgeführt, etwa weil ein Framework den Einbau-Code bei einem Rerender wiederholt, passiert nichts.
Zwei Feinheiten sind dabei wichtig. Erstens: document.currentScript liefert nichts, wenn der Code aus einem Rückruf, einem Ereignis oder einem JavaScript-Modul heraus läuft2. Ein Widget, das darauf angewiesen ist, braucht einen Rückfall, etwa die Suche nach dem letzten Script-Tag mit dem passenden Datenattribut. Zweitens: Die Seitenadresse, die einem Gespräch zugeordnet wird, ist die Adresse zum Zeitpunkt, an dem das Gespräch eröffnet wurde. Wechselt die Besucherin danach fünfmal die Route, bleibt im Posteingang die Startadresse stehen. Das ist kein Fehler, sondern eine Entscheidung; wer die aktuelle Adresse braucht, muss sie in der Nachricht selbst erfragen.
Wo der Gesprächsverlauf liegt.
Damit ein Gespräch einen harten Neuladevorgang überlebt, muss das Widget sich Besucher- und Gesprächskennung merken. Der übliche Ort ist der localStorage des Browsers, abgelegt unter einem Präfix, das den Widget-Schlüssel enthält. Zwei Widgets auf derselben Domain, etwa auf einer Marketing-Seite und in der Anwendung dahinter, kommen sich dann nicht in die Quere. Was dort nicht hingehört, ist der Gesprächsinhalt selbst: Der liegt beim Anbieter, und das Widget lädt ihn anhand der Kennung nach. Fällt der Speicher weg, weil die Besucherin ihn löscht oder im privaten Modus surft, beginnt beim nächsten Besuch ein neues Gespräch, und das alte bleibt im Posteingang des Teams erhalten.
| Einbauort | Verhalten beim Routenwechsel | Verhalten bei hartem Neuladen |
|---|---|---|
| Als Komponente im Anwendungsbaum | Wird je nach Layout abgebaut und neu montiert, Verlauf springt oder verdoppelt sich | Neu montiert, Verlauf aus dem Speicher nachgeladen |
| Als Skript am Dokument, einmal montiert | Unberührt, Gespräch bleibt offen | Neu montiert, Verlauf aus dem Speicher nachgeladen |
| Über einen Tag-Manager eingefügt | Wie am Dokument, sofern der Tag nur einmal feuert | Wie am Dokument |
Shadow DOM: Was die Stiltrennung leistet und was nicht
Ein Widget bringt eigene Stile mit, und deine Seite hat eigene. Ohne Trennung greift ein globales button { border-radius: 0 } in deinem Stylesheet auch auf den Launcher des Widgets zu, und umgekehrt färbt ein zu breit formulierter Selektor des Widgets deine Formulare ein. Der Standardweg, das zu verhindern, ist Shadow DOM: Das Widget rendert in einen eigenen Teilbaum, dessen Stile von außen nicht erreicht werden und der selbst nicht nach außen wirkt. Die Mozilla-Dokumentation fasst es so zusammen: Das CSS der Seite wirkt nicht auf Knoten im Shadow DOM, und Stile im Shadow DOM wirken nicht auf den Rest der Seite3.
Vier Dinge, die die Grenze nicht aufhält.
Die Stiltrennung ist vollständig, aber sie ist nur eine Stiltrennung. Vier Dinge wandern trotzdem über die Grenze, und alle vier tauchen in Support-Tickets an Widget-Anbieter auf:
- Vererbte Eigenschaften. Schriftart, Schriftgröße und Textfarbe werden vom Host-Element geerbt, wenn das Widget sie nicht selbst setzt. Ein Widget, das seine Schrift nicht ausdrücklich festlegt, sieht auf jeder Seite anders aus.
- Tastaturfokus. Die Tabulatortaste läuft durch die ganze Seite, Shadow DOM hin oder her. Ein geöffnetes Chatfenster, das den Fokus nicht selbst hält, lässt die Tastatur hinter dem Fenster durch die Navigation der Seite wandern.
- Stapelreihenfolge. Ein Cookie-Banner mit
z-index: 99999legt sich über den Launcher, wenn das Widget einen kleineren Wert wählt. Deshalb setzen Widgets ihren Host auf den größten möglichen Wert und isolieren ihn mitisolation: isolate, damit dieser Extremwert nicht in die Stapelkontexte der Seite hineinwirkt. - Sprache und Schreibrichtung.
langunddirwerden vom Host-Element geerbt. Eine Seite in Hebräisch oder Arabisch stellt damit auch das Widget auf Rechts-nach-links um, ob es das kann oder nicht.
Ein Shadow DOM im Modus open ist dabei kein Sicherheitsmerkmal: JavaScript der Seite kann über shadowRoot weiterhin hineingreifen. Der Modus regelt nur den Zugriff per Skript, nicht die Stiltrennung, und die gilt in beiden Modi3.
Ladezeit: Eine Zeile entscheidet
Ein Fremdskript kostet Ladezeit. Die Frage ist nur, ob es die Zeit im Hintergrund kostet oder im Vordergrund, während die Besucherin auf eine leere Seite schaut. Der Unterschied ist ein Attribut: Ein Script-Tag ohne async oder defer hält das Parsen des Dokuments an, bis das Skript geladen und ausgeführt ist. Die Empfehlung der Chrome-Entwickler ist deshalb eindeutig: Fremdskripte immer asynchron laden, außer das Skript muss laufen, bevor die Seite gerendert werden kann4. Ein Support-Widget muss das nie.
Async, nicht defer, und warum das Widget keine Schriften nachlädt.
async führt das Skript aus, sobald es geladen ist, unabhängig von der Reihenfolge im Dokument. defer wartet, bis das Dokument fertig geparst ist, und hält die Reihenfolge ein4. Für ein Widget, das nichts von der Seite braucht außer dem body-Element, ist async die richtige Wahl; es darf gern früher fertig sein als der Rest. Der Einbau von Comlayer besteht deshalb aus einem einzigen Tag mit genau diesem Attribut, etwa <script src="https://app.comlayer.app/comlayer-widget.js" data-app-id="wgt_…" async></script>. Eine JavaScript-Schnittstelle zum Öffnen, Schließen oder zum Melden eines Routenwechsels gibt es nicht, und sie fehlt nicht: Das Widget braucht sie nicht, weil es außerhalb der Anwendung lebt.
Der zweite Posten auf der Ladezeit-Rechnung wird oft übersehen: Schriften. Ein Widget, das seine Hausschrift per @import von einem Schriftdienst nachlädt, löst auf jeder Kundenseite einen weiteren Abruf bei einem weiteren Dienst aus, für jede einzelne Besucherin. Das kostet Zeit und ist, je nach Dienst und Sitz, zusätzlich eine Datenübermittlung, die im Datenschutzhinweis der Kundenseite stehen müsste. Comlayer hat diesen Import aus dem Widget entfernt und fällt auf die Systemschrift zurück; das gebaute Bündel ist damit rund 34 Kilobyte groß, komprimiert übertragen, gemessen am 17. September 2026.
| Kennzahl | Schwellenwert für „gut“ | Was ein Widget dazu beiträgt |
|---|---|---|
| Largest Contentful Paint (LCP) | 2,5 Sekunden | Nur, wenn das Skript synchron lädt oder eigene Schriften nachzieht |
| INP (Reaktionszeit auf Eingaben) | 200 Millisekunden | Lange Skriptausführung im Hauptthread beim Start |
| Cumulative Layout Shift (CLS) | 0,1 | Ein Launcher, der nach dem Laden Platz beansprucht und Inhalt verschiebt |
Die drei Schwellenwerte stammen aus den Core Web Vitals und gelten jeweils für das 75. Perzentil der Seitenaufrufe, getrennt nach Mobil und Desktop5. Ein Widget, das asynchron lädt, seinen Launcher fest positioniert und keine Schriften nachzieht, taucht in keiner der drei Zahlen auf.
Einbindung über einen Tag-Manager
Viele Teams binden Fremdskripte nicht im Quelltext ein, sondern über einen Tag-Manager. Das ist für ein Support-Widget in Ordnung, mit zwei Bedingungen. Erstens muss der Tag genau einmal feuern, beim Laden des Dokuments, und nicht bei jedem virtuellen Seitenaufruf, den die Single-Page-App an den Tag-Manager meldet. Ein Tag, der auf das Ereignis „Seitenaufruf“ hört, fügt das Skript bei jedem Routenwechsel erneut ein. Ein Widget, das eine Doppelmontage selbst abfängt, verzeiht das; eines, das es nicht tut, zeigt danach zwei Launcher.
Zweitens läuft ein per Tag-Manager eingefügtes Skript unter Umständen nicht mehr als das Script-Tag, das es im Quelltext gewesen wäre. Ein Widget, das seinen Schlüssel über document.currentScript liest, findet dann nichts und braucht den oben beschriebenen Rückfall. Prüfe nach dem Einbau in der Konsole, ob genau ein Host-Element am body hängt und ob das Widget seinen Schlüssel gefunden hat. Beides sieht man in den Entwicklerwerkzeugen in Sekunden.
Einwilligung und das Laden nach Klick
In der Europäischen Union stellt sich beim Einbau eine Frage, die mit Technik nur am Rande zu tun hat: Darf das Skript sofort laden, oder erst nach einer Einwilligung? Die rechtliche Einordnung ist umstritten, und wir haben sie in einem eigenen Beitrag zum Support-Widget und der Einwilligung ausführlich dargestellt. Technisch bedeutet die vorsichtige Variante: Das Script-Tag wird nicht statisch ins Dokument geschrieben, sondern vom Einwilligungswerkzeug erzeugt, sobald die Besucherin zugestimmt hat.
Für eine Single-Page-App ändert das nichts am Grundprinzip. Auch ein nachträglich erzeugtes Script-Tag hängt sein Host-Element einmal an den body und bleibt dort über jeden Routenwechsel hinweg bestehen. Was sich ändert, ist der Zeitpunkt: Das Widget erscheint erst nach dem Klick, und ein Gespräch, das vor der Einwilligung nicht möglich war, kann auch keinen Verlauf verlieren. Wer diese Variante wählt, sollte den Platz des Launchers trotzdem freihalten, damit die Seite beim Erscheinen des Widgets nicht springt.
Die Prüfliste nach dem Einbau
Ob eine Einbindung sauber ist, lässt sich in wenigen Minuten prüfen. Die folgenden Punkte decken die Fehler ab, die in der Praxis am häufigsten auftreten:
- 01Ein Gespräch eröffnen, dann dreimal die Route wechseln. Das Fenster bleibt offen, der Verlauf bleibt stehen, es entsteht kein zweites Gespräch.
- 02Die Seite hart neu laden. Der Verlauf ist nach dem Laden wieder da, und die Gesprächskennung im Speicher ist dieselbe wie vorher.
- 03In den Entwicklerwerkzeugen prüfen, dass genau ein Host-Element am
bodyhängt, auch nach mehreren Routenwechseln und einem Wechsel zurück zur Startroute. - 04Das Cookie-Banner öffnen, während das Chatfenster offen ist. Beide müssen bedienbar bleiben, keines darf das andere verdecken.
- 05Mit der Tabulatortaste durch das geöffnete Chatfenster gehen. Der Fokus muss sichtbar sein und darf nicht hinter dem Fenster in der Seite verschwinden.
- 06Im Netzwerk-Reiter prüfen, dass nach dem Widget-Skript keine Schriftdatei von einem fremden Dienst geladen wird.
- 07Die Seite mit den Lighthouse-Werkzeugen messen, einmal mit und einmal ohne Widget. Die drei Core Web Vitals dürfen sich nicht verschlechtern.
Comlayer besteht diese Liste, weil das Widget genau dafür gebaut ist: ein Script-Tag, ein Host am body, ein Shadow DOM mit eigenen Stilen, keine Fremdschriften, keine Schnittstelle, die ein Framework bedienen müsste. Wie das Widget aussieht und welche Bausteine es trägt, stellst du im Dashboard ein, ohne die Seite neu zu bauen; was es mit den Daten der Besucherinnen tut, steht auf der Seite zu Sicherheit und Datenschutz.
Häufig gestellte Fragen
Muss ich das Widget bei jedem Routenwechsel neu initialisieren?
Nein. Ein Widget, das einmal am Dokument montiert wird und nicht im Anwendungsbaum lebt, bleibt über jeden clientseitigen Routenwechsel hinweg bestehen. Eine Initialisierung pro Route ist nur nötig, wenn das Widget als Komponente in die Anwendung eingebaut wurde, und genau das sollte vermieden werden.
Was passiert, wenn das Skript zweimal eingefügt wird?
Das hängt vom Widget ab. Ein sauber gebautes Widget prüft vor der Montage, ob sein Host-Element schon existiert, und tut beim zweiten Mal nichts. Ein Widget ohne diese Prüfung zeigt zwei Launcher und eröffnet unter Umständen zwei Gespräche. Tag-Manager, die bei jedem virtuellen Seitenaufruf feuern, sind die häufigste Ursache.
Schützt Shadow DOM mein Stylesheet vollständig vor dem Widget?
Für Selektoren ja: Stile der Seite erreichen das Shadow DOM nicht, und Stile des Widgets erreichen die Seite nicht. Vererbte Eigenschaften wie Schriftart und Textfarbe, der Tastaturfokus, die Stapelreihenfolge und die Attribute `lang` und `dir` wandern trotzdem über die Grenze.
Async oder defer für das Widget-Skript?
Async. Das Widget braucht nichts von der Seite außer dem `body`-Element und darf laufen, sobald es geladen ist. `defer` wäre nicht falsch, wartet aber unnötig auf das Ende des Parsens. Wichtig ist nur, dass eines der beiden Attribute gesetzt ist; ohne beide blockiert das Skript das Rendern.
Wie erkenne ich, ob das Widget die Ladezeit verschlechtert?
Miss die Seite mit den Lighthouse-Werkzeugen einmal mit und einmal ohne das Skript und vergleiche LCP, INP und CLS. Ein asynchron geladenes Widget mit fest positioniertem Launcher und ohne nachgeladene Schriften verändert die drei Werte nicht messbar.
Kann ich das Widget erst nach der Einwilligung laden?
Ja. Das Script-Tag wird dann nicht statisch ins Dokument geschrieben, sondern vom Einwilligungswerkzeug erzeugt, sobald die Besucherin zugestimmt hat. Auch ein so erzeugtes Tag montiert das Widget einmal am Dokument, und es überlebt danach jeden Routenwechsel wie ein statisch eingebundenes.