Warum Resolver zu langsam werden
Eine langsame GraphQL-Query ist selten GraphQLs Schuld. Die eigentlichen Probleme liegen tiefer: N+1 in Resolver-Ketten, EAV-Tabellen, die für jedes Produkt dutzende Joins erzeugen, und Queries ohne Komplexitätsgrenzen, die den Server beliebig belasten können. Dieser Artikel zeigt, wo man suchen muss und was wirklich hilft.
Inhaltsverzeichnis
- 1. Der häufigste Performance-Irrtum bei GraphQL
- 2. Das N+1-Problem: die häufigste Ursache
- 3. EAV in Magento: versteckte Datenbankkosten
- 4. Query-Tiefe und Komplexität ohne Limit
- 5. Performance messen: welche Tools helfen
- 6. Batching-Strategien: IDs sammeln, einmal laden
- 7. Query-Patterns im Performance-Vergleich
- 8. Response-Caching und feldspezifischer Cache
- 9. pageSize kontrollieren: kleine Grenzen, große Wirkung
- 10. Zusammenfassung
- 11. FAQ
1. Der häufigste Performance-Irrtum bei GraphQL
Wenn Magento GraphQL-Queries langsam sind, ist der erste Verdächtige meistens das falsche Ziel: „GraphQL ist langsam" oder „der Endpoint skaliert nicht". In Wirklichkeit ist der GraphQL-Layer selbst – das Parsen der Query, die Validierung gegen das Schema, die JSON-Serialisierung – extrem schnell. Was langsam wird, ist die Schicht darunter: die Datenbankabfragen, die ein Resolver auslöst, und die Art, wie Resolver-Ketten aufgebaut sind.
Dieses Missverständnis hat praktische Konsequenzen: Teams suchen Performance-Gewinne im falschen Bereich – durch Wechsel auf REST oder durch graphql-interne Optimierungen, die kaum messbar sind – statt sich auf die eigentlichen Bottlenecks zu konzentrieren. Ein gut gebauter Magento-GraphQL-Resolver, der eine einzelne, indizierte Datenbankabfrage macht, ist schnell. Ein Resolver, der 50 Produkte lädt und für jedes Produkt separat die EAV-Attribute abfragt, ist langsam – und das hat mit GraphQL nichts zu tun.
2. Das N+1-Problem: die häufigste Ursache
Das N+1-Problem ist das häufigste Performance-Antipattern in GraphQL-Systemen – und in Magento besonders leicht zu reproduzieren. Es entsteht, wenn eine Query N Elemente aus einer Liste lädt (1 Abfrage) und dann für jedes dieser Elemente ein Child-Feld mit einer eigenen Datenbankabfrage auflöst (N weitere Abfragen). Bei einer Produktliste mit 20 Artikeln, die alle related_products anfragen, entstehen so 21 Datenbankabfragen, obwohl eine einzige Batch-Abfrage ausreichend wäre.
In Magento ist das Problem besonders tückisch, weil viele Child-Resolver intern das Repository-Pattern nutzen und dadurch transparent wirken: $this->productRepository->getById($productId) sieht harmlos aus, löst aber für jeden Aufruf eine separate SELECT-Query aus. Die Lösung ist Batching: statt für jedes Element separat zu laden, sammeln die Child-Resolver alle benötigten IDs in einer Struktur und führen am Ende eine einzige Abfrage für alle IDs durch. In Magento wird das typischerweise über einen RequestContext oder eine BatchLoader-Klasse implementiert.
# ANTI-PATTERN: This query triggers N+1 if related_products resolver
# is not batched — 1 query for products + 1 query per product for related items
query ExpensiveWithN1 {
products(search: "jacket", pageSize: 20) {
items {
sku
name
related_products { # Each product triggers a separate DB query
sku
name
price_range { minimum_price { final_price { value } } }
}
}
}
}
# BETTER: Avoid deep nesting if the data is not critical for this view
# Only request what the current page component actually renders
query ProductListOptimized {
products(search: "jacket", pageSize: 20) {
total_count
items {
sku
name
url_key
price_range { minimum_price { final_price { value currency } } }
small_image { url label }
}
}
}
3. EAV in Magento: versteckte Datenbankkosten
Das Entity-Attribute-Value-Modell (EAV) von Magento ist der wahrscheinlich größte Performance-Faktor bei Produktabfragen über GraphQL. Magento speichert Produktattribute nicht in einer einzigen breiten Tabelle, sondern in typisierten Wertetabellen wie catalog_product_entity_varchar, catalog_product_entity_int und catalog_product_entity_decimal. Für jedes Attribut, das in einer GraphQL-Query angefragt wird, können mehrere Joins entstehen – und das multipliziert mit der Anzahl der Produkte in der Ergebnisliste.
Magento hat für dieses Problem teilweise Gegenmaßnahmen eingebaut: Der Flat-Catalog-Index materialiert die EAV-Daten in eine breite Tabelle, die deutlich effizienter abzufragen ist. Aber der Flat-Catalog ist nicht immer aktiviert, und custom Attribute, die über eigene Module hinzugefügt werden, sind oft nicht im Flat-Catalog enthalten. Für GraphQL-Resolver, die produktbezogene Daten laden, lohnt es sich, gezielt zu messen, wie viele Datenbankabfragen pro Request entstehen – mit dem Magento-Profiler oder einem Query-Logger im Debug-Modus.
4. Query-Tiefe und Komplexität ohne Limit
GraphQL erlaubt von Haus aus beliebig tiefe und komplexe Queries. Eine Query, die Produkte abfragt, für jedes Produkt verwandte Produkte lädt und für jedes verwandte Produkt wieder verwandte Produkte anfrägt, ist syntaktisch valide und würde ohne Schutzmaßnahmen ausgeführt. In Magento kann eine solche Query exponentiell viele Datenbankabfragen auslösen und den Server unter Last setzen – auch für anonyme Requests, die kein Sicherheitstoken benötigen.
Magento 2.4 hat begrenzte eingebaute Schutzmaßnahmen gegen Query-Komplexität. Für produktive Systeme empfiehlt es sich, ergänzend auf der Nginx- oder Varnish-Ebene Request-Limits zu setzen und im Resolver selbst defensive Limits für pageSize-Parameter durchzusetzen. Ein Resolver, der pageSize: 9999 akzeptiert, ohne zu begrenzen, ist eine potenzielle Ressourcenfalle. Die Prüfung sollte im Resolver früh erfolgen und mit einer GraphQlInputException bei Überschreitung des Maximums enden.
# Query complexity example — nested related_products creates exponential resolver calls
# Depth 3: products → related_products → related_products (recursive)
query DangerousDepth {
products(search: "shoe", pageSize: 50) {
items {
sku
related_products {
sku
related_products {
sku
price_range { minimum_price { final_price { value } } }
}
}
}
}
}
# SAFE alternative: flat query, controlled pageSize, no recursive nesting
query ControlledComplexity {
products(
search: "shoe"
pageSize: 12 # Enforce reasonable page sizes in resolver
currentPage: 1
) {
total_count
items {
sku
name
price_range { minimum_price { final_price { value } } }
}
}
}
5. Performance messen: welche Tools helfen
Bevor man Performance-Probleme in Magento GraphQL behebt, muss man wissen, wo sie liegen. Der Magento-eigene Query-Logger lässt sich im Admin unter Stores > Configuration > Advanced > Developer aktivieren und zeigt alle SQL-Abfragen, die während eines Requests ausgeführt werden, mit Ausführungszeit. Für eine GraphQL-Produktlistenabfrage, die 50 SQL-Queries auslöst, ist das die schnellste Methode, das Ausmaß des Problems zu sehen.
Für tiefergehende Analyse bieten sich APM-Tools wie New Relic, Blackfire.io oder die Magento-eigene Profiling-Infrastruktur an. Blackfire ist besonders hilfreich, weil es Resolver-zu-Resolver-Profiling auf PHP-Ebene zeigt und damit sichtbar macht, in welcher Klasse und welcher Methode die meiste Zeit verbracht wird. Ein Profiling-Lauf für eine typische Produktlisten-Query zeigt oft, dass 80 % der Zeit in Repository-Calls oder EAV-Ladevorgängen verbracht wird – nicht in der GraphQL-Schicht selbst.
6. Batching-Strategien: IDs sammeln, einmal laden
Batching ist die wirksamste Strategie gegen N+1 in Magento GraphQL. Das Grundprinzip: statt jedes Child-Element einzeln aus der Datenbank zu laden, sammelt ein BatchLoader alle benötigten IDs während der Resolver-Ausführung und führt am Ende eine einzige Abfrage für alle IDs durch. Dieses Pattern erfordert eine Umstrukturierung der typischen Resolver-Implementierung, zahlt sich aber bei Listen ab 5 Elementen bereits messbar aus.
In der Praxis implementiert man Batching in Magento häufig über eine RequestContext-Klasse, die als Singleton im DI-Container lebt und während eines Requests IDs sammelt. Der erste Aufruf eines Child-Resolvers registriert eine ID; nachfolgende Aufrufe fügen ihre IDs zum BatchLoader hinzu. Nach der vollständigen Resolver-Kette werden alle gesammelten IDs in einer Abfrage geladen. Das Magento-Framework bietet keine direkte DataLoader-Abstraktion wie GraphQL.js, aber das Pattern ist mit wenig Code selbst implementierbar.
7. Query-Patterns im Performance-Vergleich
Nicht jede GraphQL-Query ist gleich teuer. Die Strukturierung der Query hat direkten Einfluss auf die Anzahl der Datenbankabfragen, die auf dem Server entstehen. Die folgende Tabelle zeigt typische Muster und ihre Performance-Implikationen.
| Query-Pattern | Datenbankabfragen | Performance-Risiko | Empfehlung |
|---|---|---|---|
| Flache Produktliste ohne Child-Felder | 1–2 Queries | Gering | Ideal für Listen-Ansichten |
| Produkte + Preise (gecacht) | 1–3 Queries | Gering | Standard-Pattern für PLPs |
| Produkte + related_products (unbatched) | N+1 pro Produkt | Sehr hoch | Batching implementieren oder vermeiden |
| Tiefe Verschachtelung (3+ Ebenen) | Exponentiell | Kritisch | Query-Tiefe begrenzen, Complexity-Limit setzen |
| pageSize: 200 ohne Limit | 200× Child-Resolver | Sehr hoch | pageSize im Resolver auf max. 48 begrenzen |
8. Response-Caching und feldspezifischer Cache
Magento unterstützt Response-Caching für anonyme GraphQL-Queries über Varnish oder den integrierten Full-Page-Cache. Wenn eine Produktlisten-Query gecacht ist, wird die gesamte Response aus dem Cache bedient, ohne dass ein einziger PHP-Request oder eine Datenbankabfrage stattfindet. Das ist die wirksamste Performance-Maßnahme für öffentlich zugängliche Produktdaten – und gleichzeitig die am häufigsten falsch konfigurierte.
Der Response-Cache greift nur, wenn die @cache-Annotation im Schema korrekt konfiguriert ist und die Identity-Klasse die richtigen Cache-Tags setzt. Fehlt die Identity-Klasse oder gibt sie leere Tags zurück, wird jeder Request neu berechnet. Für kundenbezogene Queries – Warenkorb, Kundenkonto, Wishlist – greift der Response-Cache grundsätzlich nicht. Hier hilft feldspezifisches Caching auf Service-Ebene: Produktdetails, die sich selten ändern, können im Resolver mit einem kurzen TTL gecacht werden, um wiederholte EAV-Abfragen zu vermeiden.
9. pageSize kontrollieren: kleine Grenzen, große Wirkung
Einer der einfachsten Performance-Hebel in Magento GraphQL ist die Kontrolle des pageSize-Parameters in Resolver-Implementierungen. Standardmäßig ist die Magento-API so konfiguriert, dass ein pageSize: 300-Request syntaktisch valide ist und ausgeführt wird. Das bedeutet: 300 Produkte werden geladen, ihre Preise berechnet, ihre Attribute aus EAV-Tabellen zusammengezogen, ihre Bilder aufgelöst und in die Response serialisiert.
Ein eigener Resolver sollte den pageSize-Parameter immer gegen einen konfigurierbaren Maximalwert prüfen und bei Überschreitung eine GraphQlInputException werfen. Für die meisten Use Cases sind 12–24 Produkte pro Seite ausreichend und sinnvoll. Darüber hinaus lohnt es sich, die Magento-interne pageSize-Konfiguration über den Admin zu überprüfen und bei Bedarf anzupassen. Diese einzelne Maßnahme kann die durchschnittliche Response-Zeit einer Produktlisten-Query um Faktoren reduzieren.
# Controlled product list query with explicit pagination and minimal fields
# This pattern is safe for production: bounded pageSize, no deep nesting
query ProductListSafe {
products(
filter: { category_id: { eq: "15" } }
pageSize: 24
currentPage: 1
sort: { name: ASC }
) {
total_count
page_info {
current_page
page_size
total_pages
}
items {
id
sku
name
url_key
price_range {
minimum_price {
regular_price { value currency }
final_price { value currency }
discount { amount_off percent_off }
}
}
small_image { url label }
rating_summary
}
}
}
10. Zusammenfassung
Langsame Magento GraphQL-Resolver haben fast immer dieselben Ursachen: N+1 in Resolver-Ketten, EAV-Abfragen ohne Batching, fehlende pageSize-Limits und nicht genutzter Response-Cache. Das GraphQL-Framework selbst ist dabei nicht das Problem – der Overhead liegt in den Datenbankabfragen, die Resolver auslösen. Profiling mit dem SQL-Query-Logger oder Blackfire zeigt schnell, wo die Zeit wirklich verbracht wird.
Die wirksamsten Maßnahmen in der Praxis: pageSize im Resolver deckeln, Batching für Child-Resolver implementieren, Response-Cache für anonyme Queries aktivieren und korrekt konfigurieren sowie tiefe Verschachtelungen in Queries verhindern. Jede dieser Maßnahmen kann die Performance einer typischen Produktlisten-Query um 50–80 % verbessern – ohne die GraphQL-API grundlegend zu verändern.
Magento GraphQL Performance — Das Wichtigste auf einen Blick
N+1 identifizieren
SQL-Query-Logger aktivieren: mehr als 10 Queries pro GraphQL-Request ist ein klares Signal für N+1 oder fehlende Batching-Strategie.
EAV-Overhead
Flat-Catalog aktivieren, benutzerdefinierte Attribute auf den Flat-Catalog abbilden und EAV-Abfragen im Resolver durch gecachte Zwischenschicht entlasten.
pageSize begrenzen
Maximale pageSize im Resolver prüfen und mit GraphQlInputException begrenzen. Empfehlung: max. 24–48 für Produktlisten.
Response-Cache
Für anonyme Queries immer @cache mit Identity-Klasse konfigurieren. Der Cache-Hit macht Datenbankabfragen vollständig überflüssig.
11. FAQ: Magento GraphQL Performance
1Ist GraphQL generell langsamer als Magento REST?
2Wie erkenne ich ein N+1-Problem?
3Was ist Batching und wie implementiere ich es?
4Warum ist EAV ein Performance-Problem?
5Wie begrenzt man pageSize in einem Resolver?
resolve(): if ($args['pageSize'] > 48) { throw new GraphQlInputException(...); }. Maximalwert konfigurierbar halten.6Für welche Queries funktioniert Response-Caching?
7Wie aktiviere ich den SQL-Query-Logger?
bin/magento dev:query-log:enable. Logs in var/debug/db.log.