Geschwindigkeit: wann eine Komponente neu rendert
Kurz gesagt: In Vue rendert die Komponente neu, die einen geänderten Wert liest, und ein Kind, wenn seine Props neu sind; du misst zuerst, dann hält computed dasselbe Ergebnis.
Problem
Jeder Buchstabe rendert die ganze Liste
Oben gibt es ein Suchfeld und einen Button „Suchen“. Die Liste wird erst nach „Suchen“ gefiltert. ProductList schreibt bei jedem Rendern in die Konsole. Tippe „milch“ ins Feld und sieh in die Konsole.
Konsole
Fünf Buchstaben, fünf neue Zeilen „rendere ProductList“. Die Liste ist dieselbe: Du hast noch nicht auf „Suchen“ geklickt. Hier gibt es acht Produkte, und du merkst es nicht. In einem echten Katalog sind es 2000, jedes mit Bild und Preis, und das Feld beginnt, deinen Fingern hinterherzuhinken.
Die Idee
Wann eine Komponente neu rendert
Eine Komponente rendert neu, wenn sich die Daten ändern, die sie zeigt. Aber was „ihre Daten“ sind, entscheiden die beiden Bibliotheken verschieden. Beide liefern das richtige Ergebnis; der Unterschied ist, wie viel Arbeit sie dafür leisten.
Vue verfolgt, welche reaktiven Werte jede Komponente beim Rendern liest. Ändert sich draft, rendert Vue nur die Komponenten neu, die draft gelesen haben: hier App. Ein Kind rendert neu, wenn seine Props sich von den vorigen unterscheiden (es vergleicht sie einzeln).
Der Zähler in ProductList.vue steckt in onBeforeMount und onBeforeUpdate: Vue ruft sie vor dem ersten und vor jedem weiteren Rendern der Komponente auf.
Hier übergibt das Template :products="visible()": eine Funktion, die filtert. Sie läuft bei jedem Rendern von App und gibt ein neues Array zurück. Für Vue ist das eine neue Prop, also rendert es auch ProductList.
Erst messen, dann reparieren. Ein Zähler in der Konsole wie hier ist die einfachste Messung. Die Vue DevTools haben eine Timeline mit den Renderings der Komponenten und ihren Zeiten. Du reparierst nur ein Rendern, das überflüssig und langsam ist.
Sieh es dir an
Sieh, wer aufleuchtet
<script setup>const draft = ref('');const query = ref('');const visible = computed(() => filterByName(PRODUCTS, query.value));</script>
<template> <input v-model="draft" /> <button @click="query = draft">Suchen</button> <ProductList :products="visible" @add="add" /></template>
<!-- ProductList.vue --><script setup>defineProps(['products']);onBeforeUpdate(() => console.log('rendere ProductList'));</script>Schritt für Schritt, in Worten
- App hält den Text des Feldes (draft) und die Suche (query). ProductList zeichnet die Karten. Die Konsole zählt, wie oft die Liste rendert.
- Vor der Korrektur rief das Template eine Funktion auf, visible(). Jedes Rendern von App erzeugte ein neues Array, deshalb sah Vue eine neue Prop und renderte ProductList neu.
- computed hält dasselbe Array, solange query dasselbe ist. Vue verfolgt, was jede Komponente liest, und vergleicht die Props des Kindes.
- Jetzt rendert eine Taste nur App und das Feld. Die Props von ProductList sind dieselben, also wird sie übersprungen, und der Zähler bleibt.
- „Suchen“ ändert query. Das Array ist neu, also rendert ProductList. Dieses Rendern ist nötig: Die Liste zeigt jetzt zwei Produkte.
- Erst messen: ein Zähler in der Konsole oder der Profiler in den DevTools. Repariere nur die Renderings, die überflüssig und langsam sind.
computed hält das Array, während eine Funktion im Template bei jedem Rendern ein neues liefert.Probier es aus
Nur „Suchen“ rendert die Liste
Sorge dafür, dass Tippen ins Feld und „Hinzufügen“ ProductList nicht noch einmal rendern. Nur „Suchen“ soll sie rendern, denn nur dann ändert sich die Liste. Die Prüfung tippt „milch“, fügt zwei Produkte hinzu, klickt auf „Suchen“ und zählt die Zeilen „rendere ProductList“ in der Konsole.
Konsole
Lösung
App.vue<script setup>
import { ref, computed } from 'vue';
import ProductList from './ProductList.vue';
import { PRODUCTS } from './products.js';
const draft = ref('');
const query = ref('');
const count = ref(0);
const visible = computed(() => PRODUCTS.filter((p) => p.name.toLowerCase().includes(query.value.toLowerCase())));
function add() {
count.value++;
}
function search() {
query.value = draft.value;
}
</script>
<template>
<header>
<form role="search" @submit.prevent="search">
<input type="search" aria-label="Suche" v-model="draft" />
<button>Suchen</button>
</form>
<p>Im Warenkorb: {{ count }}</p>
</header>
<main>
<ProductList :products="visible" @add="add" />
</main>
</template>
ProductList.vue<script setup>
import { onBeforeMount, onBeforeUpdate } from 'vue';
import { money } from './products.js';
defineProps(['products']);
const emit = defineEmits(['add']);
let renders = 0;
const log = () => console.log(`rendere ProductList: ${++renders}`);
onBeforeMount(log);
onBeforeUpdate(log);
</script>
<template>
<ul>
<li v-for="p in products" :key="p.id">
<span>{{ p.name }}, {{ money(p.price) }}</span>
<button type="button" :aria-label="`Hinzufügen ${p.name}`" @click="emit('add', p)">
Hinzufügen
</button>
</li>
</ul>
</template>
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 2, name: 'Vollkornbrot', price: 1.9 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 6, name: 'Fettarme Milch', price: 0.9 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 8, name: 'Kaschkawal', price: 9.5 },
{ id: 9, name: 'Butter', price: 3.4 },
{ id: 10, name: 'Eier (10 Stk.)', price: 2.8 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button, input { font: inherit; }
Die Funktion visible() wird zu computed: const visible = computed(() => PRODUCTS.filter(…));, und das Template übergibt :products="visible", ohne Klammern. computed rechnet nur neu, wenn sich query ändert, und gibt bis dahin dasselbe Array zurück. add ist eine normale Funktion: Sie ist bei jedem Rendern dieselbe, und Vue behält auch die Pfeile in @add="…".
Die Prüfung erkennt auch :products="[...visible]": Die Kopie ist bei jedem Rendern ein neues Array.
Probier es aus
Finde den Fehler
visible ist computed, und die Liste rendert trotzdem bei jedem Buchstaben. Welche Zeile ist schuld?Wähle die Zeile mit dem Fehler.
[...visible] erzeugt bei jedem Rendern von App ein neues Array, obwohl visible dasselbe ist. Übergib es direkt: :products="visible".Probier es aus
Ergänze
Wie bleibt die teure Liste zwischen den Renderings dieselbe?
const visible = (
() => PRODUCTS.filter((p) => p.name.includes(query.value)),
);computed: führt die Funktion aus, wenn sich etwas ändert, das sie liest (query.value). Sonst gibt es dasselbe Array zurück.Die Idee
Eine lange Liste: weniger rendern
Das schnellste Element ist das, das nicht gezeichnet wird. Ein Mensch sieht 10–20 Produkte auf dem Bildschirm, nicht 2000. Also zeichnet eine lange Liste nur einen Teil:
- „Mehr anzeigen“: die ersten 20, dann bei einem Klick 20 weitere. Gemerkt wird nur, wie viele angezeigt werden.
- Seiten: „1, 2, 3 …“, oft mit der Nummer in der Adresse (
/?page=2), damit man sie teilen kann. - Eine virtuelle Liste (Windowing): zeichnet nur die Zeilen, die sichtbar sind, und tauscht sie beim Scrollen aus. Sie ist schwer zu schreiben, deshalb nimmst du eine Bibliothek: TanStack Virtual oder vue-virtual-scroller.
Die Schlüssel aus der Lektion über Listen helfen auch hier: Mit key={p.id} (:key="p.id") sind die 20 hinzugefügten Produkte neue Elemente, und die alten bleiben bestehen, ohne neu gemacht zu werden.
Die Idee
Lazy Loading
Nicht jeder Teil der Seite wird sofort gebraucht. Die Bewertungen eines Produkts, die Karte der Läden, der Profil-Editor: Sie können erst laden, wenn der Mensch sie öffnet. Dann lädt das erste Laden weniger Code herunter.
import { defineAsyncComponent } from 'vue';
const Reviews = defineAsyncComponent(() => import('./Reviews.vue'));import('./Reviews.vue') ist ein Funktionsaufruf, der die Datei lädt und ein Promise zurückgibt. defineAsyncComponent macht daraus eine Komponente, die wie jede andere benutzt wird: <Reviews v-if="open" />.
Konsole
Öffne die Konsole: Die Zeile aus reviews.js kommt erst nach dem Klick. Um ehrlich zu sein: In der Sandbox sind alle Dateien schon in der Seite, deshalb führt import() die Datei hier nur später aus. In einem echten Projekt legt Vite jede solche Datei getrennt ab, und der Browser lädt sie beim ersten Klick vom Server. Dasselbe gilt für Seiten: In Vue Router lädt component: () => import('./CartPage.vue') die Seite beim ersten Wechsel dorthin.
Genauer, für Neugierige
v-memo. Für ein sehr langes v-for hat Vue v-memo="[p.id === selected]": Die Zeile rendert nur neu, wenn sich die Werte im Array ändern. Die Dokumentation nennt es eine Mikrooptimierung für seltene Fälle. Ein falsches Array bedeutet ein verpasstes Update.
Zustand nach unten. Das Feld mit seinem draft kann eine eigene Komponente SearchBox sein, die search erst bei „Suchen“ nach oben sendet. Dann ändern die Buchstaben nur SearchBox.
computed mit stabilem Ergebnis. Ab Vue 3.4 löst ein computed, das denselben primitiven Wert zurückgibt (etwa true), nicht aus, was davon abhängt. Bei einem Objekt kannst du das vorige zurückgeben (computed((old) => …)), wenn der Inhalt derselbe ist.
Der Entwicklungsmodus ist langsamer. Warnungen und Prüfungen kosten Zeit. Miss die Geschwindigkeit der gebauten Version (vite build und vite preview), nicht nur in der Entwicklung.
Übung
Mehr anzeigen
Der Katalog hat 10 Produkte und zeichnet sie alle. Zeige die ersten 4, darunter „Angezeigt: 4 von 10“ und einen Button „Mehr anzeigen“, der 4 weitere hinzufügt. Sind alle angezeigt, verschwindet der Button. Die Prüfung zählt die Produkte nach jedem Klick und sieht sich den Text und den Button an.
Konsole
Lösung
App.vue<script setup>
import { ref, computed } from 'vue';
import { PRODUCTS, money } from './products.js';
const PAGE = 4;
const limit = ref(PAGE);
const shown = computed(() => PRODUCTS.slice(0, limit.value));
</script>
<template>
<main>
<ul>
<li v-for="p in shown" :key="p.id">
<span>{{ p.name }}, {{ money(p.price) }}</span>
</li>
</ul>
<p>{{ `Angezeigt: ${shown.length} von ${PRODUCTS.length}` }}</p>
<button v-if="limit < PRODUCTS.length" type="button" @click="limit += PAGE">Mehr anzeigen</button>
</main>
</template>
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 2, name: 'Vollkornbrot', price: 1.9 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 6, name: 'Fettarme Milch', price: 0.9 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 8, name: 'Kaschkawal', price: 9.5 },
{ id: 9, name: 'Butter', price: 3.4 },
{ id: 10, name: 'Eier (10 Stk.)', price: 2.8 },
{ id: 11, name: 'Äpfel (1 kg)', price: 2.2 },
{ id: 12, name: 'Bananen (1 kg)', price: 2.6 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button, input { font: inherit; }
Du merkst dir nur, wie viele angezeigt werden: const limit = ref(4);. Die angezeigten werden berechnet: computed(() => PRODUCTS.slice(0, limit.value)). Der Button hat v-if="limit < PRODUCTS.length", und der Klick ist limit += 4. Eine normale Variable let limit = 4 ist nicht reaktiv, und das Template rendert nicht neu: Die Prüfung erkennt das.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Effekte · Routing
- Aufgaben zum Thema: Eine ganze App