©Sergey Emelyanov 2025 | Alle Rechte vorbehalten
Wenn Sie eine Anwendung in Vue.js entwickeln, müssen Sie ständig Speicherlecks kontrollieren. Dies gilt insbesondere für SPA-Anwendungen mit dynamischem Laden von Komponenten. Aufgrund des architektonischen Designs sollten die Benutzer den Browser nicht aktualisieren, so dass die Reinigung der Komponenten und die Müllsammlung von der Anwendung selbst durchgeführt werden sollte.
Heute schauen wir uns einen realen Fall aus der E-Commerce-Praxis an, bei dem ein harmloses Kontrollkästchen „Sonderangebote anzeigen“ die Ursache für eine lawinenartige Belastung des Servers wurde.
Ein alter Freund von mir trat mit einem sehr interessanten Problem an mich heran. Er hat einen ziemlich alten Online-Shop, der in Vue.js + Symfony erstellt wurde und einige Benutzer beschwerten sich über die fehlerhafte Arbeit der Seite. Von Zeit zu Zeit im Prozess der Arbeit Online-Shop aufgehört, die Preise und die Reaktion auf Benutzer-Anfragen. Das Problem wurde dadurch erschwert, dass es unmöglich war, diesen Fehler zu reproduzieren, da er nicht bei allen Benutzern und nur periodisch auftrat.
Dennoch gelang es mir, durch die Analyse des Nutzerverhaltens und persönliche Gespräche mit den Nutzern wertvolle Informationen zu erhalten, die mir halfen, die problematische Komponente zu identifizieren und den Fehler zu beheben. Insbesondere gab einer der Nutzer an, dass er die Produktkarten durchging, das Kontrollkästchen für die Rabattverfolgung anklickte und es dann ausschaltete. Und dann ergab alles einen Sinn für mich.
Verwendungsszenario:
Ein Benutzer einer Produktseite aktiviert die Option „Echte Rabatte verfolgen“:
Hier ist die Komponente selbst, PriceTracker.component.vue (übergeordnete Komponente), wo der Fehler gefunden wurde:
<template>
<div class="product-page">
<h2>{{ product.name }}</h2>
<p>Base Price: {{ formatCurrency(product.basePrice) }}</p>
<label class="real-time-toggle">
<input type="checkbox" v-model="enableRealTimeUpdates" />
Track real-time discounts
</label>
<RealTimeDiscounts
v-if="enableRealTimeUpdates"
:product-id="product.id"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import RealTimeDiscounts from './RealTimeDiscounts.vue';
import { useProductStore } from '@/stores/product';
import { formatCurrency } from '@/utils/currency';
const productStore = useProductStore();
const product = productStore.currentProduct;
const enableRealTimeUpdates = ref(false);
</script>
Hier gibt es nichts Ungewöhnliches. Wir haben ein Kontrollkästchen, das, wenn es aktiviert ist, eine untergeordnete Komponente lädt:
RealTimeDiscounts.component.vue:
<template>
<div class="discount-widget">
<div v-if="loading" class="loading">Updating prices...</div>
<div v-else class="discounts">
<p class="discount">Current Discount: {{ currentDiscount }}%</p>
<p class="final-price">Final Price: {{ formatCurrency(finalPrice) }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useDiscountApi } from '@/api/discount';
import { formatCurrency } from '@/utils/currency';
const props = defineProps<{
productId: string;
}>();
const { fetchLiveDiscount } = useDiscountApi();
const currentDiscount = ref(0);
const loading = ref(false);
let updateInterval: number | undefined;
const startPriceUpdates = () => {
updateInterval = window.setInterval(async () => {
loading.value = true;
try {
const discount = await fetchLiveDiscount(props.productId);
currentDiscount.value = Math.max(
currentDiscount.value,
discount.value
);
} catch (error) {
console.error('Discount update failed:', error);
} finally {
loading.value = false;
}
}, 30000); // Каждые 30 секунд
};
onMounted(startPriceUpdates);
const finalPrice = computed(() => {
return product.basePrice * (1 - currentDiscount.value / 100);
});
</script>
In diesem Beispiel laden wir eine Komponente, die eine API-Anforderung stellt, und verwenden dann die Schaltfläche „Anzeigen/Ausblenden“ mit der v-if-Direktive, um sie dem virtuellen DOM hinzuzufügen oder daraus zu entfernen. Das Problem in diesem Beispiel ist, dass die v-if-Direktive das übergeordnete Element aus dem DOM entfernt, aber wir haben die zusätzlichen DOM-Fragmente, die von der untergeordneten Komponente erstellt wurden, nicht bereinigt, was zu einem Speicherleck führt.
Das Ergebnis ist folgendes:
Wie das Ganze aus der Sicht des Benutzers aussieht:
Die einfachste Variante, wie Sie einen solchen Fehler beheben können:
// RealTimeDiscounts.component.vue
import { onUnmounted } from 'vue';
const abortController = new AbortController();
const startPriceUpdates = () => {
updateInterval = window.setInterval(async () => {
if (abortController.signal.aborted) return;
try {
const discount = await fetchLiveDiscount(
props.productId,
{ signal: abortController.signal }
);
currentDiscount.value = discount.value;
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Discount update failed:', error);
}
}
}, 30000);
};
onUnmounted(() => {
clearInterval(updateInterval);
abortController.abort();
});
import { debounce } from 'lodash-es';
const updateDiscount = debounce((newValue: number) => {
currentDiscount.value = newValue;
}, 1000);
Außerdem müssen wir einige Umstrukturierungen vornehmen, um nicht nur den Fehler zu beheben, sondern auch die Leistung insgesamt zu verbessern.
let retryCount = 0;
const baseDelay = 30000;
const startPriceUpdates = () => {
updateInterval = window.setInterval(async () => {
try {
await fetchDiscount();
retryCount = 0;
} catch (error) {
retryCount++;
const delay = baseDelay * Math.pow(2, retryCount);
setTimeout(startPriceUpdates, delay);
return;
}
}, baseDelay);
};
const metrics = {
requests: 0,
errors: 0,
lastUpdate: null as Date | null
};
setInterval(() => {
if (metrics.requests > 0) {
sendMetricsToServer(metrics);
}
}, 300000);
Wie kann man einen solchen Fehler erkennen?
Seien Sie vorsichtig mit Speicherlecks auf der Client-Seite. Ein solcher Fehler kann dazu führen:
Der Fehler mit „vergessenen“ Timern ist ein klassisches Beispiel für ein Problem, das:
Allgemeine Regel: Jede Ressource, die in onMounted erstellt wird, muss einen expliziten Cleanup-Handler in onUnmounted haben.
©Sergey Emelyanov 2025 | Alle Rechte vorbehalten