{ }
type
Magento GraphQL · Kategorieseiten · Performance · Caching · Produktliste
Kategorieseiten über Magento
GraphQL optimieren

Die Kategorieseite ist der wichtigste Performance-Hotspot in fast jedem Magento-Shop. Zu viele Felder, zu tiefe Resolver-Ketten und fehlende Caching-Strategien machen sie langsamer als nötig. Der Hebel liegt in der Query – nicht im Server.

16 Min. Lesezeit Produktliste · Aggregationen · Caching · N+1 · Resolver-Ketten Magento 2.4 · GraphQL · Hyvä · Headless

1. Warum Kategorieseiten der wichtigste GraphQL-Hotspot sind

Die Kategorieseite ist in fast jedem Magento-Shop der meistbesuchte Seiten-Typ und gleichzeitig der teuerste GraphQL-Request. Eine typische Kategorieseite lädt eine Produktliste mit Bildern, Preisen und Varianten, dazu Filter-Aggregationen, Gesamtanzahl für Paginierung und Kategoriemetadaten. All das kommt in einer einzigen GraphQL-Query – und wenn diese Query nicht sorgfältig designed ist, treibt sie Resolver-Kosten in die Höhe, die weder durch Hardware noch durch Caching vollständig kompensierbar sind.

Der entscheidende Unterschied zu REST: Bei REST ist die Antwortstruktur serverseitig definiert. Bei GraphQL entscheidet der Client, welche Felder er anfordert – und damit auch, wie teuer der Request wird. Frontends, die bei der Entwicklung einfach "alles verfügbare" anfordern, erzeugen in Magento Resolver-Ketten, die EAV-Attribute, Bilder, Preise und Lagerstatus für jedes Produkt einzeln abfragen. Das Ergebnis sind hunderte Datenbankzugriffe für eine einzige Seite.

2. Anatomie einer Kategorieseiten-Query

Eine vollständige Kategorieseiten-Query in Magento hat typischerweise drei Hauptabschnitte: die Produktliste mit den Feldern, die für die Kacheln auf der Seite gerendert werden, die Aggregationen für die Filter-Seitenleiste und page_info für die Paginierung. Dazu kommen oft Kategoriemetadaten, die für SEO, Breadcrumbs und Seiten-Header gebraucht werden.

Jeder dieser Abschnitte hat ein anderes Caching-Profil und eine andere Resolver-Tiefe. Die Produktliste ist am teuersten, weil jedes Produkt individuelle EAV-Attribute, Preisregeln und Bestandsinformationen hat. Aggregationen sind günstiger, weil sie auf Datenbankebene aggregiert werden. Paginierungsinformationen sind billig. Kategoriemetadaten sind meist cachebar. Wer diese vier Abschnitte kennt und ihren relativen Kostenbeitrag versteht, kann die Query gezielt optimieren.


# Optimized category page query — only request fields that are actually rendered
# Split into logical sections with documented cost profile

query CategoryPage(
  $categoryId: String!
  $pageSize: Int!
  $currentPage: Int!
  $sort: ProductAttributeSortInput
  $filter: ProductAttributeFilterInput
) {
  # Section 1: category metadata (cheapest — cacheable)
  categoryList(filters: { ids: { eq: $categoryId } }) {
    uid
    name
    description
    meta_title
    meta_description
    image
    breadcrumbs {
      category_id
      category_name
      category_url_key
    }
  }

  # Section 2: product listing (most expensive — EAV, prices, images)
  products(
    filter: $filter
    pageSize: $pageSize
    currentPage: $currentPage
    sort: $sort
  ) {
    # Section 3: pagination info (cheap)
    total_count
    page_info {
      current_page
      page_size
      total_pages
    }

    # Section 4: product tiles — minimal fields only
    items {
      uid
      sku
      name
      url_key
      url_suffix

      # Image: request only the required size
      small_image {
        url
        label
      }

      # Price: minimum_price only — avoid full price_range for listings
      price_range {
        minimum_price {
          regular_price { value currency }
          final_price { value currency }
          discount { amount_off percent_off }
        }
      }

      # Rating for listing display
      rating_summary
      review_count
    }
  }
}

3. Felder reduzieren: Nur anfordern, was gerendert wird

Das wirksamste Optimierungsmittel bei Kategorieseiten-Queries ist das konsequente Reduzieren auf die Felder, die tatsächlich auf der Seite gerendert werden. Jedes zusätzliche Feld in einer Produktlisten-Query kann einen oder mehrere Datenbankzugriffe pro Produkt auslösen. Bei 24 Produkten pro Seite bedeutet jedes überflüssige Feld potenziell 24 unnötige Queries.

Die häufigsten Kandidaten für Felder, die angefordert aber nicht gerendert werden: description (langer HTML-Text, der auf der Detailseite angezeigt wird, nicht auf der Listenseite), short_description (oft nicht in Kacheln dargestellt), media_gallery (alle Bilder statt nur small_image), related_products (Resolver-Explosion: für jedes Produkt werden verwandte Produkte geladen) und price_range mit maximum_price (teurer als minimum_price allein). Ein systematischer Abgleich zwischen den angeforderten Feldern und dem tatsächlichen Rendering-Template ist die effizienteste Art, versteckte Performance-Verluste aufzudecken.

4. Aggregationen und Filter effizient modellieren

Aggregationen sind die Filter-Optionen in der Seitenleiste: Farbe, Größe, Preis, Marke, Bewertung. In Magento kommen sie über das aggregations-Feld der Produkt-Query und werden von OpenSearch berechnet. Das bedeutet, dass Aggregationen eine separate Anfrage an den Search-Backend darstellen – teuer, aber cacheable. Der wichtige Optimierungspunkt ist die Auswahl der aggregierten Attribute: Wenn alle 50 EAV-Attribute als aggregierbar markiert sind, wird die Aggregations-Berechnung entsprechend aufwändiger.

Praktische Empfehlung: Nur Attribute als filterable markieren, die tatsächlich in der Seitenleiste angezeigt werden. Das reduziert die Aggregations-Kosten und verkleinert den Response-Payload. Im GraphQL-Query kann man außerdem die Anzahl der Aggregations-Optionen über aggregations { options(limit: 10) } begrenzen – für Attribute mit vielen möglichen Werten (z.B. Hersteller mit hunderten Optionen) ist eine sinnvolle Beschränkung eine bessere UX und schneller zu rendern.


# Aggregations query — efficient filter sidebar loading
# Request only needed aggregation attributes, limit options per filter

query CategoryFilters(
  $filter: ProductAttributeFilterInput!
  $pageSize: Int!
) {
  products(filter: $filter, pageSize: $pageSize) {
    total_count

    # Aggregations for filter sidebar — only requested attributes returned
    aggregations(filter: { position: { from: "0" to: "100" } }) {
      attribute_code
      label
      count

      # Limit options to prevent huge payloads for attributes with many values
      options {
        label
        value
        count
      }
    }

    items {
      uid
      sku
      name
      url_key
      small_image { url label }
      price_range {
        minimum_price {
          final_price { value currency }
        }
      }
    }

    page_info {
      current_page
      page_size
      total_pages
    }
  }
}

5. N+1-Probleme in Resolver-Ketten erkennen und beheben

Das N+1-Problem ist das häufigste Performance-Antipattern in GraphQL-APIs und tritt in Magento-Kategorieseiten besonders deutlich auf. Das Grundmuster: Ein Resolver für eine Liste lädt N Produkte, dann ruft für jedes Produkt ein weiterer Resolver ein zugehöriges Objekt ab – N weitere Queries. Bei 24 Produkten werden so aus einer Query 25 Datenbankzugriffe. Bei tiefer Verschachtelung (Produkte → Kategorien → übergeordnete Kategorien) wachsen die Zugriffe exponentiell.

In Magento ist das typische N+1-Szenario die Kombination aus Produktliste und individuellen Attributen oder Kategoriezuordnungen. Magento selbst verwendet intern Batching-Mechanismen für viele Felder, aber eigene Erweiterungen, die Custom Resolver implementieren, sind häufig anfällig. Die Lösung ist entweder DataLoader-ähnliches Batching (alle IDs sammeln, in einer Query laden) oder das Verlegen der Datenzugriffe auf eine Ebene höher, sodass alle benötigten Daten in der übergeordneten Resolver-Methode vorgeladen werden. Das Werkzeug zur Diagnose: bin/log graphql.log und Query-Profiling mit aktiviertem Debugging.

6. Paginierung: Cursor vs. Offset in Magento

Magento GraphQL nutzt Offset-basierte Paginierung über currentPage und pageSize. Das ist die einfachere der beiden Paginierungsarten und für E-Commerce-Kategorieseiten mit typischen Seitengrößen von 12 bis 48 Produkten durchaus geeignet. Cursor-basierte Paginierung wäre für sehr große Datensätze performanter (weil kein OFFSET in SQL), ist aber in Magento nativ nicht verfügbar und muss für Custom-Implementierungen explizit gebaut werden.

Ein wichtiger Performance-Aspekt bei Offset-Paginierung: Je tiefer die Seite, desto teurer die Datenbankabfrage. Seite 100 mit einer Seitengröße von 24 bedeutet, dass MySQL oder OpenSearch die ersten 2376 Ergebnisse verwerfen muss, bevor die Seite 100 ausgeliefert werden kann. Für Kategorieseiten, die normalerweise nicht tief paginiert werden, ist das kein praktisches Problem. Für Importer oder Crawler, die systematisch alle Seiten durchgehen, kann das zum Engpass werden. In solchen Fällen empfiehlt sich ein separater API-Endpunkt mit Cursor-Paginierung oder einem ID-basierten Iterationsansatz.

7. Caching-Strategie für Kategorieseiten-Queries

Kategorieseiten sind einer der cachbarsten Inhalte im Shop: Produktlisten ändern sich nicht bei jedem Request, Preise bleiben für Standardkunden gleich und Aggregationen sind stabil bis zum nächsten Katalog-Update. Die Caching-Strategie für Kategorieseiten-Queries hat drei Ebenen: HTTP-Caching für anonyme Requests, serverseitiges Query-Result-Caching für komplexe Resolver-Ergebnisse und Edge-Caching über CDN für geografisch verteilte Traffic-Spitzen.

In Magento ist das integrierte Varnish-Caching für GraphQL-Endpunkte standardmäßig aktiv. Das Problem: Magento invalidiert den Cache aggressiv bei Katalog-Änderungen – eine Preisänderung für ein einziges Produkt kann den Cache für hunderte Kategorieseiten leeren. Für Headless-Setups mit Hyvä oder einem separaten Frontend-Server empfiehlt sich ein granularerer Invalidierungsansatz über Cache-Tags: Jede Query-Antwort wird mit den relevanten Produkt- und Kategorie-IDs getaggt, und Invalidierung erfolgt selektiv nur für betroffene Seiten.

8. Typische Fehler bei Kategorieseiten-Queries

Der häufigste Fehler bei Kategorieseiten-Queries ist das Anfordern von related_products oder upsell_products im Kontext der Produktliste. Diese Felder triggern für jedes Produkt in der Liste einen weiteren Resolver-Aufruf, der seinerseits wieder eine vollständige Produktliste lädt. Das erzeugt eine exponentiell wachsende Anzahl von Datenbankzugriffen, die selbst bei kleinen Produktlisten zu Timeouts führen kann. Verwandte Produkte gehören auf die Detailseite, nicht in die Kategorielistenquery.

Ein zweiter typischer Fehler: Die Query-Variablen werden nicht über GraphQL-Variablen übergeben, sondern direkt in die Query eingebettet. Das verhindert Persisted Queries und macht HTTP-Caching deutlich ineffizienter, weil jede Query-Variation als separater Cache-Key behandelt wird. Alle variablen Teile einer Kategorieseiten-Query – Filter, Seitenzahl, Seitengröße, Sortierung – gehören als Variablen-Argumente übergeben, damit Tooling und Caching optimal funktionieren.

Problem Symptom Lösung Aufwand
Zu viele Felder Hohe Query-Zeit, großer Payload Felder auf tatsächlich gerenderte beschränken Gering
N+1 in Resolver Viele SQL-Queries, langsame Seiten Batching im Resolver einführen Mittel
related_products in Liste Exponentieller Query-Anstieg Feld aus Kategorieliste entfernen Gering
Kein Query-Caching Jede Seite frisch aus DB geladen Varnish / CDN-Caching konfigurieren Mittel
Hardcodierte Werte in Query Cache-Fragmentierung, kein Persisted-Query Alle variablen Werte als Variablen übergeben Gering

9. Falsch vs. Richtig: Query-Vergleich

Der direkteste Weg, den Nutzen von Kategorieseiten-Optimierungen zu demonstrieren, ist der Vergleich zweier Queries für denselben Anwendungsfall: eine unoptimierte Query, die alles verfügbare anfordert, und eine optimierte Query, die nur das rendert, was die Seite braucht. Der Unterschied in der Resolver-Tiefe und der Anzahl der Datenbankzugriffe ist oft erheblich – zweistellige Faktoren bei der Query-Zeit sind in der Praxis keine Seltenheit.

Die optimierte Query ist nicht weniger "korrekt" als die unoptimierte – sie ist bewusst eingeschränkt. Diese Bewusstheit ist der Unterschied zwischen einem GraphQL-Endpunkt, der zufällig performant ist, und einem, der strukturiert für Performance designed wurde. Die bewusste Einschränkung gehört dokumentiert: Warum wird description nicht angefordert? Weil sie auf der Kategorienseite nicht gerendert wird und auf der Detailseite separat geladen werden kann. Diese Dokumentation gehört als Kommentar in die Query-Fixture-Datei, nicht in ein Wiki-Dokument, das niemand liest.


# WRONG: Over-fetching on category listing page
# Causes: deep resolver chains, N+1 for related products, large payload

query CategoryPageBad($categoryId: String!) {
  products(filter: { category_id: { eq: $categoryId } }, pageSize: 24) {
    items {
      sku
      name
      url_key
      description { html }          # never shown in listing — expensive
      short_description { html }     # usually not shown in listing either
      media_gallery {                # all images — only thumbnail needed
        url
        label
        position
      }
      price_range {
        minimum_price { regular_price { value } final_price { value } discount { amount_off percent_off } }
        maximum_price { regular_price { value } final_price { value } }  # not needed
      }
      related_products {             # N+1 explosion: 24 * N extra queries
        sku
        name
        price_range { minimum_price { final_price { value } } }
      }
      upsell_products { sku name }   # same issue as related_products
      crosssell_products { sku }
    }
  }
}

# RIGHT: Lean category listing — only fields actually rendered
# Each field justified by a component that renders it

query CategoryPageGood(
  $filter: ProductAttributeFilterInput!
  $pageSize: Int!
  $currentPage: Int!
  $sort: ProductAttributeSortInput
) {
  products(
    filter: $filter
    pageSize: $pageSize
    currentPage: $currentPage
    sort: $sort
  ) {
    total_count
    page_info { current_page page_size total_pages }

    items {
      uid
      sku                     # needed for cart operations
      name                    # rendered in product tile
      url_key                 # needed for product link
      small_image { url label }  # single thumbnail image

      price_range {
        minimum_price {
          regular_price { value currency }
          final_price { value currency }
          discount { percent_off }        # for sale badge
        }
      }

      rating_summary          # star rating in tile
      review_count            # review count badge
    }
  }
}

Kategorieseiten über Magento GraphQL optimieren — Das Wichtigste auf einen Blick

Felder reduzieren

Nur Felder anfordern, die auf der Kategorieseite tatsächlich gerendert werden. description, media_gallery und related_products gehören nicht in die Listenseiten-Query.

N+1 beheben

Resolver-Ketten mit Batching absichern. related_products in der Produktliste ist das häufigste N+1-Muster – immer entfernen aus Kategorieseiten-Queries.

Variablen nutzen

Filter, Sortierung und Paginierung immer als GraphQL-Variablen – nicht hardcodiert. Ermöglicht Persisted Queries und effizientes HTTP-Caching.

Caching aktivieren

Varnish-Caching für anonyme Requests, granulare Cache-Tag-Invalidierung für Headless-Setups. Kategorieseiten sind hoch cacheable.

10. Zusammenfassung

Kategorieseiten über Magento GraphQL zu optimieren bedeutet vor allem, bewusste Entscheidungen darüber zu treffen, welche Felder wirklich gebraucht werden und welche Resolver-Tiefe vertretbar ist. Der größte Hebel liegt in der Query selbst – nicht in der Infrastruktur. Eine schlanke, gut gebaute Kategorieseiten-Query mit korrekten Variablen, ohne N+1-Felder und mit passendem Caching-Header liefert bessere Ergebnisse als ein überdimensionierter Server hinter einer schlecht optimierten Query.

Die praktischen Schritte in der richtigen Reihenfolge: Zuerst messen – welche Felder werden tatsächlich gerendert, wie lange dauert die Query, wie viele SQL-Queries werden erzeugt? Dann reduzieren – alle Felder entfernen, die das Rendering nicht braucht. Dann N+1-Muster identifizieren und Batching einführen. Schließlich Caching konfigurieren und validieren, dass Invalidierung korrekt funktioniert. Diese Reihenfolge stellt sicher, dass Optimierungsaufwand dort investiert wird, wo der tatsächliche Engpass liegt.

11. FAQ: Kategorieseiten über Magento GraphQL optimieren

1Welche Felder sind besonders teuer in der Kategorieliste?
description, media_gallery (alle Bilder), related_products und upsell_products – sie triggern für jedes Produkt zusätzliche Resolver-Aufrufe.
2Was ist ein N+1-Problem bei Kategorieseiten?
Pro Produkt in der Liste ein individueller Datenbankzugriff statt einer gemeinsamen Abfrage. related_products ist das häufigste Beispiel – bei 24 Produkten entstehen 24 extra Queries.
3Wie viele Felder braucht eine optimale Kategorieliste?
So wenige wie möglich: sku, name, url_key, small_image, minimum_price, rating_summary. Alles was nicht auf der Kachel gerendert wird, gehört nicht in die Query.
4Warum Filter als GraphQL-Variablen?
Hardcodierte Werte verhindern Persisted Queries und fragmentieren den HTTP-Cache. Variablen ermöglichen eine einzige Query-Definition für alle Parameter-Kombinationen.
5Wie cache ich Kategorieseiten-Queries?
Varnish für anonyme Requests. Für Headless: granulare Cache-Tag-Invalidierung, damit Preisänderungen nicht den gesamten Kategorieseiten-Cache leeren.
6Cursor vs. Offset-Paginierung in Magento?
Für normale Kategorieseiten mit 12-48 Produkten ist Offset ausreichend. Cursor sinnvoll für sehr große Datensätze oder systematisches Durchgehen aller Produkte.
7N+1-Probleme in Magento identifizieren?
bin/log graphql.log und Developer-Mode-Profiling. SQL-Query-Anzahl ein Vielfaches der Produktanzahl ist sicheres N+1-Zeichen.
8Aggregationen in der Filterseitenleiste optimieren?
Nur benötigte Attribute als filterable markieren. Im Query Options-Anzahl für Attribute mit vielen Werten begrenzen. Aggregationen sind teuer aber cacheable.
9related_products auf Kategorieseite laden?
Nein. Erzeugt N+1 für jedes Produkt. Gehört auf die Detailseite, wo sie für ein einzelnes Produkt geladen werden.
10Richtige Optimierungsreihenfolge?
Messen → Felder reduzieren → N+1 beheben → Caching konfigurieren. Immer in dieser Reihenfolge – ohne Messen wird Zeit falsch investiert.