sinnvoll einsetzen in Vue 3
Vue 3 brachte drei Features mit sich, die selten zusammen erklärt werden, aber sich gegenseitig ergänzen: Teleport für korrektes DOM-Placement, Suspense für deklarative Ladezustände und Async Components für gezieltes Code-Splitting. Wer alle drei versteht und sinnvoll kombiniert, baut Vue-Anwendungen mit besserer UX, kleineren Bundles und saubererer Komponentenarchitektur.
Inhaltsverzeichnis
- 1. Überblick: Teleport, Suspense und Async Components
- 2. Teleport: DOM-Placement aus dem Komponentenbaum lösen
- 3. Teleport für Modals, Toasts und Overlays
- 4. Async Components mit defineAsyncComponent
- 5. Lade- und Fehlerzustände in Async Components
- 6. Suspense: deklarative Ladezustände im Template
- 7. Suspense mit Async Components kombinieren
- 8. Wann welches Feature sinnvoll ist
- 9. Vergleich: Einsatzszenarien im Überblick
- 10. Zusammenfassung
- 11. FAQ
1. Überblick: Teleport, Suspense und Async Components
Vue Teleport, Vue Suspense und Async Components adressieren drei verschiedene Probleme im Vue-Komponentenmodell. Teleport löst das DOM-Placement-Problem: Eine Komponente, die im Komponentenbaum tief verschachtelt ist, soll ihren DOM-Output an einer anderen Stelle im Dokument rendern — typischerweise für Modals und Overlays, die direkt unter body gerendert werden müssen, um korrekte CSS-Stacking-Context-Probleme zu vermeiden. Das Problem ist alt, die Lösung in Vue 3 ist elegant und deklarativ.
Vue Suspense löst das Ladezustands-Problem für async-fähige Komponenten. Statt in jeder Komponente individuell v-if="isLoading" zu verwalten, deklariert man im Template einen Fallback-Slot, der angezeigt wird, bis alle asynchronen Kinder ihren Datenbedarf erfüllt haben. Async Components ergänzen Suspense mit Code-Splitting: Die Komponente und ihr JavaScript werden erst dann vom Server geladen, wenn die Komponente tatsächlich gerendert werden soll. Das reduziert den initialen Bundle-Size und beschleunigt den First Meaningful Paint.
Die Kombination aller drei: Eine Route lädt via Router lazy. Die Hauptkomponente der Route nutzt Vue Suspense als Wrapper. Innerhalb des Suspense-Wrappers befinden sich Async Components, die ihre Daten in setup() via await laden. Ein Modal-Composable im Seitenbereich nutzt Vue Teleport, um das Modal an body anzuhängen statt in die verschachtelte DOM-Struktur der Route. Diese Architektur ist wartbar, performant und vermeidet die typischen CSS-Probleme mit Overlays in tief verschachtelten Komponenten.
2. Teleport: DOM-Placement aus dem Komponentenbaum lösen
Vue Teleport ist ein eingebauter Wrapper, der seinen Slot-Inhalt an einem anderen DOM-Knoten rendert als dem, wo die Komponente im Vue-Baum sitzt. Das to-Attribut akzeptiert einen CSS-Selektor oder ein DOM-Element-Objekt. Die reaktive Logik der teleportierten Komponente — Props, Emits, Reaktivität — bleibt vollständig im Vue-Komponentenbaum erhalten. Nur die DOM-Position ändert sich. Das ist der entscheidende Unterschied: Vue Teleport ist keine echte Entkopplung der Komponente, sondern nur eine Neupositionierung ihres DOM-Outputs.
Das klassische Problem ohne Vue Teleport: Ein Modal wird in einer tief verschachtelten Komponente definiert. Der CSS-overflow: hidden oder transform eines Vorfahren-Elements erzeugt einen neuen Stacking Context. Das Modal, obwohl es position: fixed hat, ist auf diesen Stacking Context beschränkt und erscheint nicht korrekt über allen anderen Elementen. Mit Vue Teleport wird der Modal-DOM direkt unter body gerendert — außerhalb jedes problematischen Stacking Contexts — während die Vue-Logik in der ursprünglichen Komponente verbleibt.
<!-- ConfirmModal.vue — uses Vue Teleport to render outside component tree -->
<template>
<!-- Teleport renders DOM to body, but component stays in Vue tree -->
<Teleport to="body">
<Transition name="modal-fade">
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex items-center justify-center"
role="dialog"
aria-modal="true"
:aria-labelledby="`modal-title-${uid}`"
@keydown.escape="$emit('close')"
>
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50" @click="$emit('close')" />
<!-- Panel — outside any parent overflow:hidden or transform context -->
<div class="relative z-10 bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4">
<h2 :id="`modal-title-${uid}`" class="text-xl font-bold mb-4">{ { title } }</h2>
<slot />
<div class="flex gap-3 mt-6 justify-end">
<button @click="$emit('close')">Abbrechen</button>
<button @click="$emit('confirm')">Bestätigen</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { getCurrentInstance } from 'vue'
// Unique ID per instance — accessible for aria-labelledby
const uid = getCurrentInstance()?.uid
defineProps<{ isOpen: boolean; title: string }>()
defineEmits<{ close: []; confirm: [] }>()
</script>
3. Teleport für Modals, Toasts und Overlays
Die häufigsten Anwendungsfälle für Vue Teleport sind Modals, Toast-Benachrichtigungen und Dropdown-Overlays. Bei Toasts besteht das Problem, dass sie aus beliebiger Tiefe des Komponentenbaums ausgelöst werden, aber immer an derselben Stelle im Dokument erscheinen sollen — üblicherweise oben rechts oder unten rechts, fixiert über dem gesamten Seiteninhalt. Das Muster: Ein globaler Toast-Container, der via Vue Teleport direkt unter body gerendert wird, und eine Pinia-Store- oder Composable-basierte Toast-API, die von jeder Komponente aufgerufen werden kann.
Beim Einsatz von Vue Teleport für Dropdowns — z.B. in Tabellenzeilen oder Karten, die in scrollbaren Containern sitzen — gilt dasselbe Prinzip: Das Dropdown-Panel wird zu body teleportiert, die Positionierung erfolgt über JavaScript mit getBoundingClientRect() des Trigger-Elements. Das vermeidet den overflow: auto-Clip des scrollbaren Containers. Der Mehraufwand ist das manuelle Positioning-Update bei Scroll- und Resize-Events — @floating-ui/vue nimmt diese Arbeit ab und ist die empfohlene Lösung für komplexe Dropdown-Positionierungen mit Vue Teleport.
4. Async Components mit defineAsyncComponent
Async Components in Vue 3 werden mit defineAsyncComponent() erstellt. Die Funktion nimmt eine Factory-Funktion, die ein Promise zurückgibt — typischerweise ein dynamisches Import-Statement. Vite und Webpack erkennen dynamische Imports und erstellen automatisch separate Chunks für die betreffenden Komponenten. Diese Chunks werden erst geladen, wenn die Komponente tatsächlich gerendert wird. Bei großen Komponenten wie Editoren, Chart-Libraries oder komplexen Formularen ist das ein erheblicher Ladezeit-Vorteil für alle Nutzer, die diese Teile der Anwendung nie sehen.
Der Einsatz von Async Components mit defineAsyncComponent ist besonders in Kombination mit dem Vue Router sinnvoll. Router-Level-Lazy-Loading ist für jede Route Standard — aber innerhalb einer Route gibt es oft weitere schwere Komponenten, die nicht sofort benötigt werden: ein schwerer Rich-Text-Editor, der erst beim Klick auf "Bearbeiten" erscheint, oder eine Daten-Tabellen-Komponente, die erst nach dem Laden der initialen Seitendaten eingeblendet wird. Hier setzen Async Components an — feingranularer als Router-Level-Splitting, aber ohne manuelles Bundle-Management.
5. Lade- und Fehlerzustände in Async Components
Async Components via defineAsyncComponent() können mit Loading- und Error-Komponenten konfiguriert werden, die angezeigt werden, während die Komponente geladen wird oder wenn der Ladevorgang fehlschlägt. Das Optionsobjekt unterstützt loadingComponent, errorComponent, delay (Millisekunden bevor Loading-Komponente erscheint) und timeout (nach dem die Error-Komponente gezeigt wird). Der delay-Parameter verhindert das "Flash of Loading State" bei schnellen Netzwerken: Wenn die Komponente in unter 200ms geladen ist, erscheint die Loading-Komponente gar nicht.
Das Zusammenspiel von Async Components mit errorComponent und timeout ist besonders für mobile Nutzer mit instabiler Verbindung wichtig. Statt einer hängenden leeren Stelle in der UI erscheint nach dem konfigurierten Timeout eine Fehlermeldung mit einer Retry-Option. Die errorComponent empfängt einen error-Prop mit dem aufgefangenen Fehler und kann daraus spezifische Fehlermeldungen ableiten — "Keine Verbindung", "Modul nicht gefunden" oder eine generische Fallback-Meldung. Für SSR müssen Async Components spezifisch konfiguriert werden, da der Server synchrones Rendering erwartet.
// defineAsyncComponent with loading, error and timeout config
import { defineAsyncComponent } from 'vue'
// Simple form: just a dynamic import — Vite creates a separate chunk
const HeavyEditor = defineAsyncComponent(
() => import('./components/RichTextEditor.vue')
)
// Full config: with loading/error components and timeout
const DataTable = defineAsyncComponent({
loader: () => import('./components/DataTable.vue'),
// Show skeleton after 200ms — avoids flash on fast connections
loadingComponent: () => import('./components/SkeletonTable.vue'),
delay: 200,
// Show error component after 8s timeout
errorComponent: () => import('./components/ErrorState.vue'),
timeout: 8000,
// Called when loader rejects — return true to retry
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry() // auto-retry up to 3 times
} else {
fail() // give up, show errorComponent
}
},
})
// Usage in template — acts like a normal component
// <DataTable :rows="rows" @sort="onSort" />
6. Suspense: deklarative Ladezustände im Template
Vue Suspense ist ein Built-in-Wrapper, der das Laden asynchroner Kindkomponenten koordiniert. Eine Komponente gilt für Vue Suspense als asynchron, wenn ihr setup()-Hook ein Promise zurückgibt — entweder durch async setup() oder durch ein Top-Level-await in <script setup>. Vue Suspense zeigt den #fallback-Slot, bis alle asynchronen Kinder aufgelöst sind, und schaltet dann auf den #default-Slot um. Das ist deklaratives Ladezustandsmanagement ohne ein einziges v-if="isLoading" in den Kindkomponenten selbst.
Der wichtige Unterschied zu manuellen Loading-States: Vue Suspense koordiniert mehrere async Kindkomponenten gleichzeitig. Wenn drei Kindkomponenten parallel Daten laden, zeigt Vue Suspense den Fallback, bis alle drei fertig sind — nicht bis die erste fertig ist, dann die zweite partiell, dann die dritte. Das verhindert das "Popcorn-Loading"-Muster, bei dem Elemente nacheinander mit Versatz einblenden und die Seite visuell springt. Der Fallback-Slot kann ein vollständiges Skeleton-Layout zeigen, das der finalen Ansicht entspricht und Layout-Shift minimiert.
7. Suspense mit Async Components kombinieren
Die leistungsstärkste Kombination ist Vue Suspense mit Async Components und Top-Level-await in <script setup>. Eine Async Component lädt ihr JavaScript erst bei Bedarf nach. Sobald das Modul geladen ist, führt <script setup> das Top-Level-await aus und holt die benötigten Daten. Vue Suspense koordiniert beide asynchronen Phasen — Modul-Download und Datenfetch — hinter einem einzigen Fallback-Slot. Das ist ein vollständiges Code-Splitting-und-Datenladen-Muster ohne manuellen Loading-State in irgendeiner Kindkomponente.
Für Server-Side Rendering ist Vue Suspense der empfohlene Mechanismus in Nuxt 3 (dort als useFetch- und useAsyncData-Integration automatisch vorhanden). Im Client-only-Einsatz mit Vite ist zu beachten: Vue Suspense ist noch als experimentell markiert (Stand Vue 3.x), was bedeutet, dass die API stabil, aber möglicherweise noch nicht vollständig für alle Nutzungsmuster dokumentiert ist. In Produktion funktioniert es zuverlässig für die beschriebenen Muster. Das Fehler-Handling erfolgt über @resolve-, @pending- und @fallback-Events von Vue Suspense oder über einen übergeordneten Error-Boundary-Mechanismus.
<!-- ProductDetail.vue — top-level await: component is "async" for Suspense -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { useProductStore } from '@/stores/product'
const route = useRoute()
const store = useProductStore()
// Top-level await — makes this component work with <Suspense>
// No isLoading flag needed: Suspense handles the waiting state
const product = await store.fetchProduct(route.params.id as string)
</script>
<template>
<!-- No v-if="isLoading" needed — Suspense handles it in the parent -->
<div>
<h1>{ { product.name } }</h1>
<p>{ { product.description } }</p>
</div>
</template>
<!-- In the parent route view: -->
<!-- <Suspense>
<template #default>
<ProductDetail /> <- async component, fetches in setup()
</template>
<template #fallback>
<ProductDetailSkeleton /> <- shown while loading
</template>
</Suspense> -->
8. Wann welches Feature sinnvoll ist
Vue Teleport ist sinnvoll, wann immer DOM-Elemente aus ihrem natürlichen Komponentenbaum-Kontext heraus gerendert werden müssen, um CSS-Stacking-Context-Probleme zu vermeiden. Das ist fast immer der Fall bei Modals, globalen Toast-Benachrichtigungen, Dropdown-Menüs in Tabellen und Tooltips in overflow: hidden-Containern. Wenn keine dieser Situationen vorliegt, ist Vue Teleport unnötig und erhöht nur die Komplexität.
Async Components sind sinnvoll für Komponenten über 50 KB komprimiert, die nicht beim initialen Seitenaufruf sichtbar sind. Rich-Text-Editoren, Chart-Libraries, komplexe Formulare mit vielen Feldern und Validierungslogik, schwere Daten-Tabellen. Für kleine Komponenten, die sofort sichtbar sind, erzeugen Async Components mehr Overhead (zusätzlicher HTTP-Request) als sie sparen. Vue Suspense ist am sinnvollsten in Kombination mit Top-Level-await in <script setup> und wenn mehrere Kinder gleichzeitig laden, deren Ladezustand koordiniert werden soll.
9. Vergleich: Einsatzszenarien im Überblick
Die drei Features lösen klar unterschiedliche Probleme und sollten nicht ausgetauscht werden. Diese Tabelle zeigt, wann welches Feature das richtige Werkzeug ist.
| Problem | Falsches Werkzeug | Richtiges Feature | Warum |
|---|---|---|---|
| Modal unter body rendern | Manuelles DOM-Append | Vue Teleport | Reaktivität bleibt im Vue-Baum |
| Schwere Komponente lazy laden | Alles im Main-Bundle | defineAsyncComponent | Separater Chunk, nur bei Bedarf |
| Ladezustand koordinieren | v-if="isLoading" in jedem Kind | Vue Suspense | Einheitlicher Fallback für alle Kinder |
| Dropdown aus overflow:hidden | CSS overflow:visible auf Container | Teleport + @floating-ui/vue | Portiertes Panel, korrektes Positioning |
| Chart erst bei Tab-Wechsel laden | Chart immer im DOM, v-show | defineAsyncComponent + v-if | Chunk nur wenn Tab aktiv ist |
Ein häufiger Fehler ist der übereilte Einsatz von Vue Suspense für Komponenten, die bereits schnell laden und deren Daten im Store cached sind. Vue Suspense mit einem Skeleton-Fallback für eine Komponente, die nach dem ersten Load immer aus dem Cache antwortet, erzeugt einen sichtbaren Skeleton-Flash, der schlechter aussieht als kein Ladezustand. Die Lösung: Vue Suspense nur einsetzen, wenn tatsächlich asynchrones Warten unvermeidlich ist, und den Store-Cache so gestalten, dass gecachte Daten synchron zurückgegeben werden.
Mironsoft
Vue 3 Performance und Architektur
Vue 3 Performance-Architektur für euer Projekt?
Wir optimieren Vue 3-Anwendungen mit gezieltem Code-Splitting, Teleport-Architektur für Overlays und Suspense-basierten Ladestrategien — messbar weniger Bundle-Size und bessere Core Web Vitals.
Bundle-Analyse
Identifikation schwerer Komponenten und Refactoring zu defineAsyncComponent
Modal-Architektur
Teleport-basierte Modal- und Toast-Systeme ohne CSS-Stacking-Probleme
Ladestrategie
Suspense-Integration mit Skeleton-Layouts für bessere Perceived Performance
10. Zusammenfassung
Vue Teleport, Vue Suspense und Async Components sind komplementäre Werkzeuge für drei spezifische Probleme. Teleport gehört für jeden Modal, jede Toast-Komponente und jeden Dropdown in einem overflow: hidden-Container zum Standard-Werkzeugkasten. defineAsyncComponent sollte für jede Komponente über 50 KB angewendet werden, die nicht beim ersten Seitenaufruf sichtbar ist. Vue Suspense koordiniert Ladezustände mehrerer async Kinder deklarativ und ersetzt das manuelle v-if="isLoading"-Pattern in den Kindkomponenten.
Die Kombination aller drei in einer Route-Architektur ergibt: Router-lazy-loading für Routen, Async Components für schwere Sub-Komponenten innerhalb einer Route, Vue Suspense als koordinierender Wrapper mit Skeleton-Fallback und Vue Teleport für alle Overlay-Elemente. Jedes Feature hat seinen klaren Einsatzbereich — die Stärke liegt darin, sie nur einzusetzen, wenn das spezifische Problem, für das sie gebaut wurden, tatsächlich vorliegt.
Teleport, Suspense und Async Components — Das Wichtigste auf einen Blick
Vue Teleport
to="body" für Modals, Toasts und Dropdowns in overflow:hidden-Containern. Reaktivität bleibt im Vue-Baum. Positionierung mit @floating-ui/vue.
Async Components
defineAsyncComponent() für Komponenten über 50 KB, die nicht sofort sichtbar sind. loadingComponent und delay für Flash-Vermeidung. onError für Retry-Logik.
Vue Suspense
Koordiniert Ladezustände mehrerer async Kinder. Fallback-Slot zeigt Skeleton. Kombiniert mit top-level await in script setup ohne manuellen isLoading-State.
Kombination
Router-lazy + Async Components + Suspense + Teleport: kleinere Bundles, bessere UX, saubere Overlay-Architektur ohne CSS-Stacking-Context-Probleme.