Entwickler, der außergewöhnliche CRM- und Laravel-Lösungen liefert

Als erfahrener Entwickler spezialisiere ich mich auf Laravel- und Vue.js-Entwicklung, die Implementierung von Vtiger CRM sowie auf vielfältige WordPress-Projekte. Meine Arbeit zeichnet sich durch kreative, dynamische und benutzerzentrierte Weblösungen aus, die individuell an die Bedürfnisse meiner Kunden angepasst werden.

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.

Vue.js Speicherleck: die Geschichte eines heimtückischen Bugs

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.

Der Kern des Problems: Zombie-Timer

Verwendungsszenario:
Ein Benutzer einer Produktseite aktiviert die Option „Echte Rabatte verfolgen“:

  1. Das Frontend beginnt, alle 30 Sekunden /api/live-discounts abzufragen
  2. Wenn die Option nicht aktiviert ist, wird die Komponente ausgeblendet, aber….
  3. der Timer läuft weiter!

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:

  1. Wenn das Kontrollkästchen umgeschaltet wird, werden neue Intervalle erstellt, die sich nach und nach im Speicher ansammeln
  2. Alte Intervalle senden weiterhin Anfragen an /api/live-discounts.
  3. langsame Antworten können in der falschen Reihenfolge eintreffen, was zu einer Wettlaufsituation führt
  4. Bei ständigem Umschalten der Checkbox - eine Lawine von Anfragen an das Backend

Wie das Ganze aus der Sicht des Benutzers aussieht:

  1. Benutzer schaltet „Echtzeit-Rabatte verfolgen“ ein
  2. Komponente startet Intervall #1
  3. nach 25 Sekunden schaltet der Benutzer das Kontrollkästchen aus.
  4. Intervall #1 läuft weiter
  5. Nach 30 Sekunden stellt Intervall #1 eine Anfrage
  6. Der Benutzer schaltet das Kontrollkästchen wieder ein
  7. Intervall #2 beginnt
  8. Jetzt senden zwei parallele Intervalle Anfragen
  9. Schließlich erhält der Benutzer bei ständigen Anfragen an den Server periodisch einen Fehler vom Server, was dazu führt, dass einige Komponenten nicht mehr die erforderlichen Informationen zur Anzeige erhalten.
  10. Die Website beginnt nach und nach immer mehr Fehler zu produzieren, bis der Benutzer die Seite komplett neu lädt.

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.

  1. exponentieller Bekoff bei Fehlern:
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);
};
  1. Warum nicht eine Statistik der Abfragen führen:
const metrics = {
  requests: 0,
  errors: 0,
  lastUpdate: null as Date | null
};

setInterval(() => {
  if (metrics.requests > 0) {
    sendMetricsToServer(metrics);
  }
}, 300000);
  1. Wir müssen auch die Server-API umgestalten:
  • Ergebnis-Caching implementieren
  • WebSockets anstelle von Polling verwenden
  • Hinzufügen einer Ratenbegrenzung für /api/live-discounts.
  • Implementierung einer Preisversionierung mit ETag-Validierung

Wie kann man einen solchen Fehler erkennen?

  1. Überwachen Sie die Anzahl der aktiven Verbindungen
  2. Analyse der Serverprotokolle, Behebung wiederholter Anfragen von einem Client
  3. Lasttests der Komponente durchführen
  4. Um dem Problem selbst auf die Spur zu kommen, verwenden Sie die Entwicklerwerkzeuge:
  • Registerkarte „Netzwerk“ - dort können Sie doppelte Anfragen schnell erkennen
  • Performance Monitor - um den Anstieg der Speichernutzung zu überwachen
  • Vue DevTools - um mehrere Instanzen der Komponente zu erkennen

Seien Sie vorsichtig mit Speicherlecks auf der Client-Seite. Ein solcher Fehler kann dazu führen:

  • 20-40% höhere Kosten für die Cloud-Infrastruktur
  • Verschlechterung der API-Antwortgeschwindigkeit um 300-500ms
  • Falsche Alarme von Überwachungssystemen
  • Falsche Anzeige von Preisen bei konkurrierenden Anfragen

Schlussfolgerung

Der Fehler mit „vergessenen“ Timern ist ein klassisches Beispiel für ein Problem, das:

  • Leicht zu erstellen mit schneller Entwicklung
  • Schwer zu erkennen ohne angemessene Überwachung
  • Teuer zu skalieren.

Allgemeine Regel: Jede Ressource, die in onMounted erstellt wird, muss einen expliziten Cleanup-Handler in onUnmounted haben.