Performance-Fallen in KI-generiertem Code: N+1-Queries, fehlende Indizes und Re-Renders

#Performance KI-generierter Code
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

KI-generierter Code ist oft flüssig. Er liest sich gut, folgt Namenskonventionen und übersteht einen schnellen manuellen Blick ohne Auffälligkeiten. Weniger verlässlich ist seine Performance. Nach der Prüfung dutzender Vibe-Coded Backends und Frontends tauchen immer dieselben langsamen Muster auf, nicht weil KI-Modelle nachlässig wären, sondern weil sie auf Korrektheit im Einzelfall optimieren, nicht auf Effizienz im Maßstab.

Dieser Beitrag behandelt die drei Performance-Fallen, die mir am häufigsten begegnen, nämlich N+1-Queries, fehlende Datenbankindizes und unnötige React-Re-Renders, mit konkreten Beispielen und Fixes, die du heute anwenden kannst.

Warum KI-Code tendenziell langsam ist

KI-Modelle lernen aus Beispielcode. Der meiste Beispielcode ist auf Verständlichkeit geschrieben, nicht auf Durchsatz. Ein Tutorial, das zeigt, wie man eine Liste von Bestellungen samt Kunden lädt, optimiert selten für den Fall mit 50.000 Bestellungen, es muss nur für den Leser funktionieren.

Wenn du eine KI aufforderst, ein Feature zu bauen, spiegelt sie dieses Tutorial-Denken. Das Ergebnis sieht richtig aus. Es liefert die korrekten Daten. Aber unter echter Last explodiert die Zahl der Datenbankabfragen, eine nicht indizierte Spalte scannt jede Zeile, oder die React-Komponente rendert bei jedem Tastendruck neu.

Der Abstand zwischen "funktioniert" und "funktioniert unter Last" ist der Ort, an dem die meisten Performance-Probleme in KI-generiertem Code sitzen.

Falle 1: N+1-Queries

Das N+1-Query-Problem ist eines der ältesten Performance-Probleme der Webentwicklung, und KI-generierter Code produziert es laufend. Das Muster ist einfach: Du lädst eine Liste von N Datensätzen und führst dann für jeden Datensatz eine weitere Abfrage aus, um verknüpfte Daten zu laden. Ergebnis: N+1 Datenbank-Roundtrips statt einem oder zwei.

So sieht es in einer PHP/Symfony-Codebasis aus:

// Was die KI generiert hat
$orders = $orderRepository->findAll();

foreach ($orders as $order) {
    echo $order->getCustomer()->getName(); // SELECT für jede Bestellung
}

Bei 500 Bestellungen laufen hier 501 Abfragen. In der Entwicklung mit 10 Testdatensätzen funktioniert die Seite einwandfrei. Im Produktivbetrieb mit echten Daten kriecht sie.

Der Fix, die Beziehung eager laden:

// Im Repository
public function findAllWithCustomers(): array
{
    return $this->createQueryBuilder('o')
        ->leftJoin('o.customer', 'c')
        ->addSelect('c')
        ->getQuery()
        ->getResult();
}

Jetzt lädst du alle Bestellungen und ihre Kunden in zwei Abfragen, je nach Konfiguration auch in einer mit JOIN. Der Unterschied in der Antwortzeit beträgt oft eine Größenordnung.

Im API-Kontext taucht dieselbe Falle bei der Serialisierung auf. Symfonys API Platform oder eine eigene REST-Schicht serialisiert eifrig verschachtelte Relationen, die die KI "der Vollständigkeit halber" aufgenommen hat, und löst damit dutzende Lazy-Loading-Abfragen pro Response aus. Der Fix ist derselbe: explizite JOIN-Fetches verwenden oder Serialisierungsgruppen so konfigurieren, dass Relationen ausgeschlossen werden, die du in diesem Endpunkt gar nicht brauchst.

So erkennst du es: Installiere den Symfony Profiler (oder Laravel Debugbar, wenn du auf Laravel bist) und lade eine Listenseite. Schau auf die Zahl der Abfragen. Wenn du für eine normale Listenseite mehr als 5 bis 10 Abfragen siehst, hast du mit hoher Wahrscheinlichkeit irgendwo ein N+1. Im Produktivbetrieb bringen Slow-Query-Logs oder ein Werkzeug wie Blackfire dasselbe Problem ans Licht.

Falle 2: Fehlende Datenbankindizes

KI-generierter Code legt korrekte Schema-Migrationen an, ergänzt aber selten Indizes über den Primärschlüssel hinaus. Das Modell weiß, dass die Spalten existieren müssen; es überlegt nicht, welche Spalten im Maßstab in WHERE-Klauseln auftauchen werden.

Was ein fehlender Index kostet:

Eine Tabelle mit einer Million Zeilen und ohne Index auf einer häufig abgefragten Spalte führt bei jeder Abfrage einen Full Table Scan aus. Bei 1.000 Zeilen ist das unsichtbar. Bei 100.000 Zeilen fühlen sich Seiten langsam an. Bei einer Million Zeilen laufen Requests in Timeouts.

Häufige Muster fehlender Indizes in KI-generierten Migrationen:

// KI-generierte Migration - korrekt, aber unvollständig
Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('status');
    $table->timestamp('expires_at')->nullable();
    $table->timestamps();
});

// Was sie haben sollte:
// $table->index('status');
// $table->index(['user_id', 'status']);
// $table->index('expires_at');

Die Spalte status wird ständig in WHERE-Klauseln auftauchen ("gib mir alle aktiven Abos"). Die Spalte expires_at wird in Ablauf-Jobs genutzt ("finde Abos, die in den nächsten 7 Tagen auslaufen"). Der zusammengesetzte Index aus user_id und status bedient das häufigste Zugriffsmuster: "Ist das Abo dieses Nutzers aktiv?"

Nichts davon steht standardmäßig in einem KI-generierten Schema.

So prüfst du auf fehlende Indizes:

In PostgreSQL findest du mit dieser Abfrage sequentielle Scans auf großen Tabellen:

SELECT schemaname, tablename, seq_scan, seq_tup_read, idx_scan
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_tup_read DESC;

Tabellen mit hohem seq_tup_read und niedrigem idx_scan scannen zu viele Daten. Prüfe dann, welche Abfragen auf diese Tabellen zugreifen und welche Spalten in deren WHERE-Klauseln stehen.

In MySQL zeigt dir EXPLAIN auf deinen langsamen Abfragen type: ALL (Full Table Scan) gegenüber type: ref oder type: range (Index genutzt). Jedes type: ALL auf einer Tabelle mit mehr als ein paar tausend Zeilen braucht Aufmerksamkeit.

Indizes nachträglich hinzuzufügen ist sicher, es bricht bestehende Abfragen nicht, es macht sie nur schneller. Der einzige Preis ist die Migrationsdauer bei großen Tabellen, die du mit Werkzeugen wie pt-online-schema-change für MySQL oder dem üblichen CREATE INDEX CONCURRENTLY in PostgreSQL beherrschst.

Für Symfony-Projekte behandle ich weitere Muster zur Schema-Optimierung ausführlicher im Rahmen von Code Quality Consulting.

Falle 3: Unnötige React-Re-Renders

Im Frontend leidet KI-generierter React-Code an einer anderen Spielart desselben Problems: Er erzeugt mehr Arbeit als nötig. Die Komponente rendert korrekt, aber sie rendert zu oft, oder sie erzeugt teure Objekte und Funktionen bei jedem Renderzyklus neu.

Muster 1: Objekt- und Array-Literale in JSX-Props

// Was die KI generiert hat
function UserList({ users }) {
  return (
    <DataTable
      columns={[
        { key: 'name', label: 'Name' },
        { key: 'email', label: 'Email' },
      ]}
      style={{ margin: '0 auto' }}
      data={users}
    />
  );
}

Jedes Rendern von UserList erzeugt ein neues columns-Array und ein neues style-Objekt. Wenn DataTable React.memo oder shouldComponentUpdate nutzt, schlagen diese Prüfungen immer fehl, weil die Referenzen neu sind, obwohl die Werte identisch sind. Die Komponente rendert bei jedem Rendern des Elternteils neu.

Der Fix:

const COLUMNS = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email' },
];

const TABLE_STYLE = { margin: '0 auto' };

function UserList({ users }) {
  return <DataTable columns={COLUMNS} style={TABLE_STYLE} data={users} />;
}

Verschiebe stabile Werte aus der Komponente heraus. Nutze useMemo für Werte, die von Props oder State abhängen, sich aber nicht bei jedem Rendern ändern sollen.

Muster 2: Inline-Event-Handler

// KI-generiert - erzeugt bei jedem Rendern eine neue Funktion
<button onClick={() => handleDelete(item.id)}>Delete</button>

Für einfache Fälle ist das weniger problematisch, als es aussieht, aber in Listen mit hunderten gleichzeitig gerenderten Einträgen summiert sich das Erzeugen so vieler Funktionsinstanzen pro Rendern. Nutze useCallback, wenn du Handler an memoisierte Kindkomponenten übergibst, oder strukturiere die Event-Delegation so um, dass die Item-ID über ein Data-Attribut übergeben wird.

Muster 3: Fehlende Dependency-Arrays bei useEffect

// KI-generiert - läuft bei jedem Rendern
useEffect(() => {
  fetchUserData(userId);
});

// So sollte es aussehen
useEffect(() => {
  fetchUserData(userId);
}, [userId]);

KI-Modelle lassen das Dependency-Array manchmal komplett weg, wodurch der Effect nach jedem Rendern läuft. In einer Komponente, die häufig rendert (Formularfelder, gefilterte Listen), flutet das deine API mit Requests.

So erkennst du unnötige Re-Renders:

Der React DevTools Profiler ist das richtige Werkzeug. Nimm eine Interaktion auf und suche nach Komponenten, die öfter rendern als erwartet. Die Ansicht "why did this render?" zeigt dir, welche Prop- oder State-Änderung es ausgelöst hat. Für einen schnelleren Blick während der Entwicklung hängt sich die Bibliothek why-did-you-render an deine Komponenten und protokolliert unnötige Re-Renders direkt in der Konsole.

Alles zusammen: Die Reihenfolge im Performance-Audit

Wenn ich eine neue Vibe-Coded Codebasis auf Performance-Probleme prüfe, gehe ich in dieser Reihenfolge vor:

  1. Datenbank zuerst. Aktiviere Slow-Query-Logs und nutze den Profiler, um Abfragezahlen und -muster auf zentralen Seiten anzusehen. N+1-Probleme und fehlende Indizes verstärken sich gegenseitig, behebe sie, bevor du das Frontend anfasst, denn Backend-Latenz überdeckt Frontend-Probleme.

  2. Größe der API-Responses. Prüfe, wie viele Daten deine Endpunkte zurückgeben. KI-generierte Serializer nehmen standardmäßig oft jedes verknüpfte Objekt mit. Straffe die Serialisierungsgruppen so, dass nur zurückkommt, was der Client tatsächlich rendert.

  3. Frontend-Re-Renders. Mit einem schnelleren Backend profilierst du die Render-Zahlen in React. Suche nach den drei Mustern oben. Stabilisiere Props und Callbacks, bevor du zu Memoisierung greifst, denn React.memo an einer Komponente, die bei jedem Rendern neue Objektreferenzen bekommt, bewirkt nichts.

  4. Caching. Erst nach dem Beheben der Ursachen ergibt Caching Sinn. Eine langsame N+1-Abfrage zu cachen verdeckt das Problem, statt es zu lösen.

Wann du Hilfe holen solltest

Wenn du ein Vibe-Coded Produkt gelauncht hast und jetzt Performance-Beschwerden von echten Nutzern bekommst, würde ich bei diesen drei Bereichen anfangen. Sie sind ohne Neuschreiben der Anwendung behebbar, erfordern aber das Lesen von Query-Ausführungsplänen, ein Verständnis von Objektidentität in React und das Wissen, welche Indizes tatsächlich helfen und welche nur Schreib-Overhead ohne Nutzen erzeugen.

Dieses Urteilsvermögen entsteht durch Wiederholung. Wenn du ein zweites Paar Augen auf einer Codebasis willst, die unter echtem Traffic langsam wird, melde dich unter hello@wolf-tech.io oder schau auf wolf-tech.io vorbei. Ein Performance-Audit dauert typischerweise ein bis drei Tage und liefert eine priorisierte Liste von Änderungen mit erwarteter Wirkung, sodass du nicht raten musst, welcher Fix am meisten bringt.

Das Ziel ist immer dasselbe: Code, der schnell geschrieben wurde, soll schnell laufen.