shallowRef, markRaw und toRaw gezielt einsetzen
Wer jedes Objekt in Vue 3 blind mit reactive umwandelt, zahlt bei großen Datenmengen einen messbaren Proxy-Overhead. Diese Vue Reaktivitäts Performance Anleitung zeigt, wie shallowRef, shallowReactive, markRaw und toRaw das Reaktivitätssystem gezielt entlasten, ohne Funktionalität zu verlieren.
Inhaltsverzeichnis
- 1. Warum Reaktivitäts-Overhead überhaupt entsteht
- 2. Das Proxy-System und seine Kosten im Detail
- 3. shallowRef und shallowReactive gezielt einsetzen
- 4. markRaw: Objekte dauerhaft von Reaktivität ausnehmen
- 5. toRaw und readonly: Rohdaten sicher lesen
- 6. Reaktivitäts-Overhead tatsächlich messen
- 7. Typische Anti-Patterns die Overhead erzeugen
- 8. Zusammenspiel mit Pinia und Composables
- 9. Reaktivitäts-Werkzeuge im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Reaktivitäts-Overhead überhaupt entsteht
Vue 3 baut sein Reaktivitätssystem auf JavaScript Proxies auf. Ruft man reactive(obj) auf, erhält man kein normales Objekt zurück, sondern einen Proxy, der jeden Lese und Schreibzugriff abfängt, Abhängigkeiten registriert und bei Änderungen betroffene Effekte neu ausführt. Für kleine, flache Objekte ist dieser Mechanismus praktisch kostenlos. Bei tief verschachtelten Strukturen mit tausenden Einträgen, etwa Produktkatalogen, Tabellenzeilen oder importierten JSON Antworten, summiert sich die Vue Reaktivitäts Performance Kosten spürbar, weil Vue rekursiv jedes verschachtelte Objekt in einen eigenen Proxy verwandelt, sobald darauf zugegriffen wird.
Das Problem zeigt sich selten sofort. Ein Formular mit zwanzig Feldern spürt keinen Unterschied. Eine Tabelle mit fünftausend Zeilen, deren komplette Datensätze in ein reactive Array geladen werden, erzeugt dagegen spürbare Verzögerungen beim initialen Rendering und bei jedem Update. Die Vue Reaktivitäts Performance hängt also direkt von der Datenmenge und der Verschachtelungstiefe ab. Genau hier setzen die flachen Varianten des Reaktivitätssystems an: shallowRef, shallowReactive, markRaw und toRaw geben Entwicklern präzise Kontrolle darüber, wie tief Vue tatsächlich reaktiv machen soll.
Wichtig ist die Einordnung: Diese Werkzeuge sind kein genereller Ersatz für ref und reactive. Sie sind gezielte Optimierungen für konkrete Engpässe, die man erst nach einer Messung einsetzen sollte. Wer sie präventiv überall verwendet, verliert unter Umständen erwünschte Reaktivität und produziert schwer auffindbare Bugs, weil Änderungen an verschachtelten Feldern dann keine Re Renders mehr auslösen.
2. Das Proxy-System und seine Kosten im Detail
Jeder Proxy in Vue 3 besitzt Handler für get, set, has, deleteProperty und weitere Traps. Beim Lesen einer Eigenschaft registriert der get Handler die aktuell laufende Effect Funktion als Abhängigkeit (Dependency Tracking). Beim Schreiben löst der set Handler alle registrierten Effekte aus (Trigger). Diese beiden Schritte kosten CPU Zeit, die bei einem einzelnen Objekt nicht ins Gewicht fällt, sich aber bei zehntausenden Objekteigenschaften zu einer messbaren Größe summiert, die die Vue Reaktivitäts Performance direkt beeinflusst.
Verschärft wird das Problem durch die Lazy Nested Proxy Strategie von Vue: Ein verschachteltes Objekt wird erst dann in einen Proxy verwandelt, wenn tatsächlich darauf zugegriffen wird. Das klingt nach einer Optimierung, bedeutet aber, dass eine einzige Iteration über ein tief verschachteltes Array potenziell tausende neue Proxies erzeugt, weil jeder Zugriff auf ein verschachteltes Objekt die Proxy Erzeugung auslöst. Bei einer Tabelle mit Zeilen, die jeweils ein Objekt mit Metadaten enthalten, entstehen so bei jedem vollständigen Durchlauf neue Proxy Instanzen, wenn das Array selbst neu zugewiesen wird.
Ein weiterer Kostenfaktor: Jeder Proxy Zugriff ist ein zusätzlicher Funktionsaufruf gegenüber einem direkten Property Zugriff auf ein normales Objekt. In heißen Pfaden, etwa innerhalb einer Render Funktion, die über tausend Zeilen iteriert und dabei mehrere verschachtelte Felder pro Zeile liest, addiert sich dieser Overhead zu spürbaren Framedrops. Die folgenden Abschnitte zeigen, mit welchen konkreten APIs sich dieser Overhead gezielt umgehen lässt, ohne auf Reaktivität dort zu verzichten, wo sie wirklich gebraucht wird.
// Naive approach: deep reactive object with thousands of nested entries
import { reactive } from 'vue'
// Every nested product object becomes its own Proxy on first access
const catalog = reactive({
products: Array.from({ length: 8000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: 19.99,
metadata: { supplier: 'Acme', warehouse: 'DE-01', tags: ['sale', 'new'] }
}))
})
// Iterating and reading nested metadata forces Vue to wrap
// every single metadata object in a new Proxy on first read
function totalTags() {
return catalog.products.reduce((sum, p) => sum + p.metadata.tags.length, 0)
}
3. shallowRef und shallowReactive gezielt einsetzen
shallowRef macht nur die oberste .value Zuweisung reaktiv, nicht aber verschachtelte Eigenschaften innerhalb des referenzierten Objekts. Ändert man ein Feld tief im Objekt, löst das keinen Re Render aus. Ersetzt man dagegen das gesamte Objekt mit myRef.value = neuesObjekt, funktioniert die Reaktivität wie gewohnt. Dieses Muster passt perfekt zu Daten, die als Ganzes ausgetauscht werden, etwa das Ergebnis eines API Calls, das nach jedem Fetch komplett neu gesetzt wird, statt einzelne Felder mutierend zu verändern.
shallowReactive verhält sich analog, aber für Objekte statt Refs: Nur die Eigenschaften der obersten Ebene sind reaktiv, verschachtelte Objekte bleiben unverändert und lösen bei Mutation keine Reaktion aus. Für die Vue Reaktivitäts Performance ist das besonders bei großen Listen relevant, deren einzelne Einträge sich selten oder nie ändern, aber deren Anzahl oder Reihenfolge sich häufig ändert. Man kombiniert dann shallowReactive für den Container mit gezielten triggerRef Aufrufen, falls doch einmal tief mutiert werden muss.
Ein praktischer Anwendungsfall: Ein Diagramm, das große Zeitreihen Datensätze visualisiert. Die Rohdaten ändern sich nur beim Neuladen komplett, niemals durch einzelne Feldänderungen. Mit shallowRef spart man sich die komplette rekursive Proxy Erzeugung für tausende Datenpunkte und erreicht dieselbe Funktionalität mit deutlich weniger Overhead.
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef: only the top-level .value assignment is reactive
const chartData = shallowRef([])
async function loadChartData() {
const response = await fetch('/api/timeseries')
// Full replacement triggers reactivity, no deep Proxy wrapping needed
chartData.value = await response.json()
}
// shallowReactive: top-level properties are reactive, nested objects are not
const tableState = shallowReactive({
rows: [], // reassigning tableState.rows triggers updates
page: 1,
sortBy: 'name'
})
function replaceRows(newRows) {
tableState.rows = newRows // reactive, triggers re-render
}
function mutateRowDeep(row) {
row.status = 'archived' // NOT reactive with shallowReactive container
triggerRef(tableState) // manual escape hatch if truly needed
}
4. markRaw: Objekte dauerhaft von Reaktivität ausnehmen
markRaw markiert ein Objekt dauerhaft als nicht konvertierbar. Selbst wenn man es später innerhalb eines reactive Containers ablegt, bleibt es ein reines JavaScript Objekt ohne Proxy Wrapper. Das ist die richtige Wahl für Objekte, die niemals selbst beobachtet werden müssen, aber aus praktischen Gründen in reaktivem State abgelegt werden. Typische Kandidaten sind Instanzen von Drittanbieter Bibliotheken wie Chart.js, Leaflet Karten, Editor Instanzen wie CodeMirror oder große, unveränderliche Referenzdaten wie Icon Sets oder Übersetzungstabellen.
Der Performance Vorteil entsteht, weil Vue bei markRaw markierten Objekten die komplette rekursive Proxy Erzeugung überspringt. Für die Vue Reaktivitäts Performance bedeutet das: Ein Chart.js Instanz Objekt mit hunderten internen Referenzen wird nicht in tausende verschachtelte Proxies verwandelt, nur weil es zufällig in einer reactive Komponente als Referenz gespeichert wird. Ohne markRaw würde Vue versuchen, jede interne Eigenschaft der Bibliothek in einen Proxy zu wrappen, was bei komplexen Objektgraphen zu Fehlern oder unnötigem Overhead führen kann.
Ein zweiter wichtiger Aspekt: markRaw verhindert auch subtile Bugs. Manche Bibliotheken erwarten, dass ihre internen Objekte exakt die Referenz sind, die sie selbst erzeugt haben, nicht ein Proxy Wrapper darüber. Ohne markRaw kann es passieren, dass interne Identitätsprüfungen der Bibliothek fehlschlagen, weil instanceof Checks oder Referenzvergleiche mit einem Proxy anders reagieren als mit dem Original.
import { reactive, markRaw } from 'vue'
import Chart from 'chart.js/auto'
const state = reactive({
// Without markRaw, Vue would try to deeply wrap the Chart instance
chartInstance: null,
editorInstance: null,
isLoading: false
})
function initChart(canvas) {
const instance = new Chart(canvas, { type: 'line', data: {} })
// markRaw prevents Vue from proxying internal Chart.js state
state.chartInstance = markRaw(instance)
}
// Also useful for large static reference data that never changes
import iconSet from './icon-set.json'
const staticIcons = markRaw(iconSet)
5. toRaw und readonly: Rohdaten sicher lesen
toRaw gibt das ursprüngliche, unverpackte Objekt hinter einem reaktiven Proxy zurück. Das ist nützlich, wenn man Daten an eine externe API übergeben muss, die Proxies nicht versteht, oder wenn man eine tiefe Kopie ohne die Reaktivitäts Overhead Kosten anlegen will, etwa mit structuredClone(toRaw(state)). Wichtig: Das mit toRaw gewonnene Objekt bleibt weiterhin mit dem Original verknüpft, Mutationen darauf umgehen zwar das Trigger System, verändern aber trotzdem die zugrunde liegenden Daten, was zu inkonsistentem State führen kann, wenn man nicht aufpasst.
Für Leseoperationen, die häufig in Schleifen stattfinden, etwa das Exportieren einer großen Tabelle als CSV, lohnt sich toRaw ebenfalls: Man vermeidet, dass jeder einzelne Zugriff während der Iteration durch das Proxy System läuft, was bei zehntausenden Zeilen einen spürbaren Unterschied für die Vue Reaktivitäts Performance macht. readonly ergänzt dieses Bild aus der anderen Richtung: Es erzeugt einen Proxy, der Schreibversuche mit einer Warnung blockiert, aber Lesezugriffe weiterhin reaktiv verfolgt, ideal für State, der an Kindkomponenten weitergereicht wird, die ihn nur anzeigen, aber nicht verändern sollen.
Ein häufiger Fehler ist, toRaw zu verwenden und das Ergebnis anschließend versehentlich wieder im Template zu binden. Da das Rohobjekt kein Proxy mehr ist, werden Änderungen daran nicht mehr automatisch im Template sichtbar. toRaw gehört daher ausschließlich in imperative, einmalige Operationen wie Serialisierung, Logging oder den Export, niemals in reaktive Bindungen.
import { reactive, toRaw, readonly } from 'vue'
const largeDataset = reactive({ rows: [/* thousands of entries */] })
function exportToCsv() {
// Bypass the Proxy system entirely for a read-heavy loop
const raw = toRaw(largeDataset)
return raw.rows.map(r => `${r.id},${r.name},${r.price}`).join('\n')
}
function deepCloneWithoutOverhead() {
// structuredClone on the raw object avoids proxying during the clone walk
return structuredClone(toRaw(largeDataset))
}
// readonly: children can read reactively but not mutate the source
const config = reactive({ theme: 'dark', locale: 'de-DE' })
export function useReadonlyConfig() {
return readonly(config)
}
6. Reaktivitäts-Overhead tatsächlich messen
Bevor man shallowRef, shallowReactive oder markRaw einsetzt, sollte man den tatsächlichen Overhead messen, statt zu raten. Die Chrome DevTools Performance Registerkarte zeigt in der Flame Graph Ansicht deutlich, wie viel Zeit in Vue internen Funktionen wie createGetter, track und trigger verbracht wird. Häufen sich diese Funktionsaufrufe in der Auswertung, ist das ein starkes Indiz für Reaktivitäts-Overhead, den die flachen Varianten reduzieren können.
Programmatisch lässt sich der Unterschied auch mit der User Timing API messen: performance.mark vor und nach dem Erzeugen eines reaktiven Objekts, anschließend performance.measure für die Differenz. Bei einem Test mit einem Array aus zehntausend verschachtelten Objekten zeigt sich typischerweise ein Faktor von drei bis acht zwischen einer vollständig tiefen reactive Struktur und einer shallowReactive Variante, abhängig von der Verschachtelungstiefe und der Anzahl der Zugriffe während der Messung.
Wichtig für die Interpretation: Der reine Erzeugungsoverhead ist oft kleiner als der Overhead durch wiederholte Zugriffe während des Renderings. Deshalb lohnt sich eine Messung, die den kompletten Lifecycle abbildet: Erzeugung, erstes Rendering, ein typisches Update Szenario. Nur so erkennt man, ob der eigentliche Engpass in der Proxy Erzeugung oder in wiederholten Get und Set Zugriffen während der Interaktion liegt.
import { reactive, shallowReactive } from 'vue'
function measureCreation(factory, label) {
performance.mark(`${label}-start`)
const result = factory()
performance.mark(`${label}-end`)
performance.measure(label, `${label}-start`, `${label}-end`)
const [entry] = performance.getEntriesByName(label)
console.log(`${label}: ${entry.duration.toFixed(2)}ms`)
return result
}
const rows = Array.from({ length: 10000 }, (_, i) => ({ id: i, meta: { tag: 'x' } }))
measureCreation(() => reactive({ rows: [...rows] }), 'deep-reactive')
measureCreation(() => shallowReactive({ rows: [...rows] }), 'shallow-reactive')
// Compare entry.duration values in the console to quantify the overhead
7. Typische Anti-Patterns die Overhead erzeugen
Das häufigste Anti-Pattern ist das unnötige Verschachteln von reactive in reactive, etwa wenn eine Komponente ein bereits reaktives Objekt aus einem Store noch einmal mit reactive() umschließt. Vue erkennt diesen Fall zwar und gibt den bestehenden Proxy zurück, aber der zusätzliche Funktionsaufruf und die Prüfung kosten unnötig Zeit, wenn das an tausenden Stellen im Code passiert. Ein zweites häufiges Muster: Große, unveränderliche Konfigurationsobjekte werden versehentlich mit reactive statt mit einer einfachen Konstante oder markRaw abgelegt, obwohl sie niemals mutiert werden.
Ein drittes Anti-Pattern betrifft Computed Properties, die auf tief verschachtelten reaktiven Strukturen basieren und bei jedem Zugriff eine komplette Traversierung auslösen. Statt eine einzelne computed Property zu bauen, die das gesamte verschachtelte Objekt durchläuft, lohnt sich häufig eine Vorverarbeitung mit shallowRef, sodass nur die tatsächlich benötigten, bereits aggregierten Werte reaktiv gehalten werden. Das reduziert nicht nur die Vue Reaktivitäts Performance Kosten, sondern macht den Datenfluss auch klarer nachvollziehbar.
Ein viertes, subtileres Problem: Arrays mit push, splice oder sort in großen reaktiven Listen lösen bei jedem Aufruf mehrere Trigger Zyklen aus, weil Vue Array Methoden intern instrumentiert. Wer viele Elemente in einer Schleife einzeln per push hinzufügt, erzeugt so viele einzelne Reaktivitäts Trigger, statt einer einzigen Aktualisierung. Das Sammeln aller neuen Elemente in einem separaten Array und ein einziger list.value = [...list.value, ...neueElemente] Aufruf am Ende reduziert die Anzahl der Trigger Zyklen drastisch.
8. Zusammenspiel mit Pinia und Composables
Pinia Stores basieren intern ebenfalls auf reactive, weshalb dieselben Überlegungen zur Vue Reaktivitäts Performance auch dort gelten. Große State Bäume in einem Store, die selten mutiert werden, aber häufig gelesen werden, profitieren davon, mit shallowRef im setup Store Syntax deklariert zu werden. In der Options Syntax von Pinia lässt sich dasselbe Verhalten über markRaw für einzelne State Felder erreichen, etwa für Instanzen von Drittanbieter Clients, die im Store gehalten werden, wie ein WebSocket Client oder ein API SDK Objekt.
Composables, die große Datenmengen kapseln, etwa ein useTableData Composable, das eine paginierte Serverantwort verwaltet, sollten den Rohdatensatz standardmäßig mit shallowRef speichern und nur die tatsächlich im Template benötigten, abgeleiteten Werte über computed reaktiv machen. Dieses Muster trennt sauber zwischen dem Transportcontainer für die Rohdaten und der eigentlichen UI relevanten Reaktivität, was sowohl die Vue Reaktivitäts Performance verbessert als auch die Testbarkeit des Composables erhöht, weil sich Rohdaten und abgeleiteter State unabhängig voneinander prüfen lassen.
9. Reaktivitäts-Werkzeuge im direkten Vergleich
Die Wahl zwischen ref, reactive, den flachen Varianten und markRaw hängt von der konkreten Zugriffsmuster ab, nicht von einer pauschalen Regel. Die folgende Übersicht ordnet die Werkzeuge nach Tiefe der Reaktivität und typischem Einsatzfall für bessere Vue Reaktivitäts Performance.
| Werkzeug | Reaktivitätstiefe | Overhead | Typischer Einsatzfall |
|---|---|---|---|
| reactive | Vollständig, rekursiv | Hoch bei großen Objekten | Kleine bis mittlere Formulare und UI State |
| shallowReactive | Nur oberste Ebene | Niedrig | Container mit selten mutierten, verschachtelten Einträgen |
| shallowRef | Nur .value Zuweisung | Sehr niedrig | Komplett ausgetauschte API Antworten, Zeitreihen |
| markRaw | Keine | Keine | Drittanbieter Instanzen, statische Referenzdaten |
| toRaw | Keine (Rohzugriff) | Keine | Export, Serialisierung, Deep Clone |
Als Faustregel gilt: Man startet mit ref und reactive, weil sie in den meisten Fällen ausreichend performant sind. Erst wenn eine Messung mit den DevTools oder der User Timing API einen konkreten Engpass zeigt, wechselt man gezielt zu einer flacheren Variante für genau den betroffenen State. Diese schrittweise Vorgehensweise verhindert vorzeitige Optimierung und hält den Code gleichzeitig lesbar, weil die flachen Varianten nur dort auftauchen, wo sie wirklich einen Unterschied für die Vue Reaktivitäts Performance machen.
Mironsoft
Vue 3 Performance Audits und Reaktivitäts-Optimierung
Vue App mit spürbarem Reaktivitäts-Overhead?
Wir analysieren eure Vue 3 Codebasis mit den Chrome DevTools, identifizieren teure Proxy Zugriffe und ersetzen sie gezielt durch shallowRef, shallowReactive und markRaw, ohne Funktionalität zu verlieren.
Performance Audit
Profiling mit Chrome DevTools und User Timing API auf konkrete Engpässe
Refactoring
Gezielter Einsatz flacher Reaktivität ohne Funktionsverlust
Pinia Store Review
State Bäume auf unnötigen Reaktivitäts-Overhead prüfen
10. Zusammenfassung
Die Vue Reaktivitäts Performance hängt direkt davon ab, wie tief und wie oft Vue über Proxies auf State zugreifen muss. reactive und ref sind für die meisten Fälle ausreichend performant, erzeugen bei großen, tief verschachtelten Datenmengen aber einen Overhead, der sich in Flame Graphs deutlich zeigt. shallowRef und shallowReactive begrenzen die Reaktivität auf die oberste Ebene und passen zu Daten, die komplett ausgetauscht statt einzeln mutiert werden. markRaw nimmt Objekte dauerhaft von der Proxy Erzeugung aus, ideal für Drittanbieter Instanzen und statische Referenzdaten. toRaw liefert Rohzugriff für Export und Serialisierung ohne Proxy Overhead.
Der wichtigste Grundsatz bleibt: Erst messen, dann optimieren. Die Chrome DevTools Performance Registerkarte und die User Timing API zeigen konkret, wo Vue tatsächlich Zeit in track und trigger Aufrufen verbringt. Nur mit dieser Datengrundlage lässt sich entscheiden, ob und wo shallowRef, shallowReactive oder markRaw tatsächlich einen messbaren Unterschied für die Vue Reaktivitäts Performance der eigenen Anwendung machen.
Vue Reaktivitäts Performance — Das Wichtigste auf einen Blick
Flache Reaktivität
shallowRef und shallowReactive begrenzen Proxy Erzeugung auf die oberste Ebene, ideal für komplett ausgetauschte Datensätze.
Objekte ausnehmen
markRaw verhindert jede Proxy Erzeugung dauerhaft, passend für Drittanbieter Instanzen und statische Daten.
Rohzugriff
toRaw für Export, Serialisierung und Deep Clone, niemals für reaktive Template Bindungen verwenden.
Messen vor Optimieren
Chrome DevTools Flame Graph und User Timing API zeigen konkrete Engpässe, bevor man optimiert.