Performance-Engpässe in Sekunden finden
Der Symfony Profiler ist mehr als die Debug-Leiste am Seitenrand. Er zeigt präzise, welche Datenbankabfragen eine Seite ausbremsen, wo Event-Listener übermäßig viel Zeit verbrauchen, welche Caches nicht greifen und wie viel Memory ein Request verbraucht – alles auf Knopfdruck, ohne einen einzigen Code-Änderung.
Inhaltsverzeichnis
- 1. Was der Symfony Profiler wirklich kann
- 2. Web Debug Toolbar: die wichtigsten Panels
- 3. N+1-Queries mit dem Doctrine-Panel finden
- 4. Event-Listener-Performance analysieren
- 5. Cache-Misses und Cache-Hits im Profiler
- 6. Timeline: Bottlenecks visuell lokalisieren
- 7. Custom Data Collector schreiben
- 8. Profiler in PHPUnit-Tests nutzen
- 9. Symfony Profiler vs. externe APM-Tools
- 10. Zusammenfassung
- 11. FAQ
1. Was der Symfony Profiler wirklich kann
Der Symfony Profiler ist ein eingebautes Diagnose-Werkzeug, das für jeden HTTP-Request eine vollständige Laufzeitaufzeichnung erstellt. Diese Profile werden als Dateien im var/cache/dev/profiler-Verzeichnis gespeichert und sind über die /_profiler-URL abrufbar. Jedes Profil enthält die Ausführungszeit aller Symfony-Kernel-Phasen, alle Datenbankabfragen mit Dauer und Ausführungsplan, alle Twig-Templates mit Rendering-Zeit, alle Cache-Operationen, Event-Listener-Ausführungszeiten und Memory-Peaks. Dieses Detailniveau ist das, was externe APM-Tools wie Datadog oder New Relic erst nach aufwändiger Instrumentierung liefern — der Symfony Profiler ist von Anfang an dabei.
Der entscheidende Vorteil des Symfony Profilers in der Entwicklungsphase: Er liefert Kontext. Eine langsame Seite mit dem Profiler zu analysieren zeigt nicht nur "200 ms für Datenbankabfragen", sondern welche exakten SQL-Queries ausgeführt wurden, welche Parameter gebunden waren und wie viele identische Queries mehrfach ausgeführt wurden. Ohne den Symfony Profiler müsste ein Entwickler diese Informationen durch manuelles Logging oder externe Tools sammeln. Mit dem Profiler dauert es Sekunden, den Verursacher einer Performance-Regression zu finden. Die Profiler-Daten sind persistiert und können auch nach dem Request noch analysiert werden — für Fehler, die nur unter bestimmten Bedingungen auftreten, ist das unschätzbar.
2. Web Debug Toolbar: die wichtigsten Panels
Die Web Debug Toolbar, die im Development-Modus am unteren Seitenrand eingeblendet wird, ist das erste Diagnosewerkzeug für Performance-Probleme in Symfony. Die Toolbar zeigt auf einen Blick: HTTP-Status, Routing-Match, Controller-Klasse, Anzahl Datenbankabfragen, Twig-Rendering-Zeit, Cache-Hits und -Misses, Memory-Peak und Gesamtausführungszeit. Eine Seite, die ungewöhnlich viele Datenbankabfragen zeigt, ist sofort als N+1-Problem identifizierbar, noch bevor man den vollständigen Symfony Profiler öffnet.
Klickt man auf eine Zahl in der Toolbar, öffnet sich der vollständige Symfony Profiler für den betreffenden Request. Das Doctrine-Panel zeigt alle SQL-Queries mit Ausführungszeit, Parameter und dem stacktrace, der die Query ausgelöst hat. Das Twig-Panel zeigt alle gerenderten Templates mit hierarchischer Struktur und Rendering-Zeiten. Das Security-Panel zeigt den authentifizierten User und alle Voter-Entscheidungen. Das Cache-Panel listet alle Cache-Operationen. Für AJAX-Requests, die keine vollständige HTML-Seite zurückgeben, ist kein Toolbar-Icon sichtbar — aber die Profile werden trotzdem gespeichert und sind über /_profiler/latest abrufbar.
<?php
// config/packages/web_profiler.yaml — Profiler configuration
// (default dev config — shown here for reference)
// web_profiler:
// toolbar: true
// intercept_redirects: false
// framework:
// profiler:
// only_exceptions: false # Profile all requests, not just failed ones
// collect: true
// dsn: 'file:%kernel.cache_dir%/profiler' # Where profiles are stored
// collect_serializer_data: true # Enable serializer panel
// To access profiles programmatically in tests:
// use Symfony\Bundle\FrameworkBundle\KernelBrowser;
// $client->enableProfiler();
// $client->request('GET', '/products');
// $profile = $client->getProfile();
// $queryCount = $profile->getCollector('db')->getQueryCount();
// Useful Profiler URLs:
// /_profiler → list of recent profiles
// /_profiler/latest → most recent profile
// /_profiler/{token} → specific profile by token
// /_profiler/{token}/db → directly to database panel
// To profile CLI commands (Symfony Console):
// Profiler data is written to var/cache/dev/profiler/
// Set SYMFONY_PROFILER=1 env var to enable profiling for console commands
3. N+1-Queries mit dem Doctrine-Panel finden
Das N+1-Query-Problem ist die häufigste Performance-Ursache in Symfony-Anwendungen mit Doctrine ORM. Das Symptom: Eine Produktliste mit 50 Produkten erzeugt 51 SQL-Queries — eine für die Produktliste, je eine für die zugehörige Kategorie jedes Produkts. Der Symfony Profiler Doctrine-Panel macht das sofort sichtbar: 51 Queries, von denen 50 nahezu identisch sind und sich nur im ID-Parameter unterscheiden. Der Stacktrace jeder Query zeigt, welcher Code-Pfad die Query ausgelöst hat.
Die Lösung für N+1-Queries mit dem Symfony Profiler als Ausgangspunkt ist das Eager Loading via Doctrine's DQL oder QueryBuilder mit addSelect und leftJoin. Statt 51 Queries werden eine Query mit einem LEFT JOIN ausgeführt — der Symfony Profiler zeigt danach eine einzige Query mit angemessener Ausführungszeit. Für komplexere Szenarien empfiehlt sich das Doctrine EXTRA_LAZY-Loading für Collections, das nur bei tatsächlichem Zugriff lädt, und die Doctrine Second-Level-Cache-Integration, die bei häufig abgerufenen Entities die Datenbankzugriffe weiter reduziert. Der Profiler bleibt das zentrale Werkzeug, um die Wirkung dieser Optimierungen zu messen.
4. Event-Listener-Performance analysieren
Der Symfony Profiler zeigt im Events-Panel alle dispatched Events und die Zeit, die jeder Listener für seine Ausführung benötigt hat. Das ist besonders wertvoll in Symfony-Anwendungen, die viele Event-Listener nutzen — API-Platform-Projekte, Sicherheitsimplementierungen mit mehreren Listenern oder Custom-Geschäftslogik über Events. Ein Listener, der bei jedem Request eine Datenbankabfrage ausführt — etwa um Benutzerberechtigungen zu laden — erscheint im Events-Panel mit seiner Gesamtdauer und kann als Performance-Problem identifiziert werden.
Ein häufiger Befund im Symfony Profiler: Der security.firewall-Listener ist langsam, weil er bei jedem Request einen User aus der Datenbank lädt, obwohl er in Session gespeichert sein sollte. Oder der kernel.request-Listener führt eine unnötige Datenbankabfrage aus, bevor der Controller überhaupt ausgeführt wird. Der Symfony Profiler zeigt die Ausführungsreihenfolge aller Listener und ihren relativen Zeitanteil an der Gesamtanfragezeit. Events mit hoher Gesamtdauer sind Kandidaten für Optimierung: Caching der Listener-Ergebnisse, Lazy-Loading von Dependencies oder Verschieben der Arbeit in asynchrone Symfony-Messenger-Messages.
5. Cache-Misses und Cache-Hits im Profiler
Das Cache-Panel im Symfony Profiler zeigt alle Cache-Operationen: Lesezugriffe (get), Schreiboperationen (set), Deletes und ob der Zugriff ein Hit oder Miss war. Ein hoher Anteil an Cache-Misses bei einem bestimmten Cache-Key ist ein Hinweis darauf, dass entweder die Cache-TTL zu kurz ist, der Cache zu oft geleert wird, oder die Cache-Key-Generierung zu granular ist. Der Symfony Profiler zeigt nicht nur ob ein Cache-Miss aufgetreten ist, sondern auch den genauen Key — das ermöglicht die Analyse, ob Keys konsistent gebildet werden oder ob unbeabsichtigt unterschiedliche Keys für identische Daten erzeugt werden.
In Symfony-Anwendungen mit HTTP-Cache gibt das Performance-Panel im Symfony Profiler Auskunft über Cache-Control-Header und ESI-Fragmente. Wenn eine Seite theoretisch cachebar ist, aber Cache-Control-Header das Caching verhindern, zeigt der Profiler warum — etwa weil ein Event-Listener Response::setPrivate() aufgerufen hat. Das Twig-Panel zeigt zusätzlich, welche Templates kompiliert und welche aus dem Opcode-Cache geladen wurden. Für Symfony-Projekte mit vielen Twig-Templates ist der Kompilierungs-Overhead beim ersten Aufruf nach einem Cache-Clear im Profiler direkt messbar.
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* Product repository with N+1-prevention via eager loading.
*/
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
/**
* Load products with category in a single query — prevents N+1.
* Before: 1 query for products + N queries for categories = N+1 problem
* After: 1 query with LEFT JOIN = Symfony Profiler shows 1 query
*
* @return Product[]
*/
public function findAllWithCategory(): array
{
return $this->createQueryBuilder('p')
// Eager-load the category relation — fetched in the same SQL query
->addSelect('c')
->leftJoin('p.category', 'c')
->orderBy('p.name', 'ASC')
->getQuery()
->getResult();
}
/**
* Use DQL with INDEX BY to build a lookup map — avoids repeated array searches.
* Result: associative array keyed by product ID.
*
* @return array<int, Product>
*/
public function findAllIndexedById(): array
{
return $this->createQueryBuilder('p', 'p.id')
->getQuery()
->getResult();
}
}
6. Timeline: Bottlenecks visuell lokalisieren
Das Timeline-Panel im Symfony Profiler ist das mächtigste Werkzeug für die Analyse komplexer Performance-Probleme. Es zeigt den gesamten Request-Lifecycle als horizontale Zeitleiste: Kernel-Bootstrap, Routing, Controller-Ausführung, Template-Rendering, Response-Finalisierung. Jede Phase ist als farbiger Balken dargestellt, der sowohl die absolute Dauer als auch den relativen Zeitanteil zeigt. Wenn der Controller-Balken 80% der Gesamtzeit einnimmt, weiß man sofort, dass die Performance-Arbeit im Controller oder in den vom Controller aufgerufenen Services beginnen muss.
Die Timeline zeigt auch verschachtelte Span-Daten: Wenn ein Service einen anderen Service aufruft, der wiederum eine Datenbankabfrage auslöst, ist die Hierarchie im Symfony Profiler Timeline sichtbar. Das macht es möglich, in einem Request mit 500 ms Gesamtdauer exakt den 200-ms-Span zu identifizieren, der eine externe HTTP-Anfrage darstellt, die blockierend auf Antwort wartet. Für Custom-Instrumentation — eigene Services, deren Ausführungszeit im Timeline erscheinen soll — verwendet man den Symfony Stopwatch-Service, der direkt mit dem Symfony Profiler integriert ist und Spans zum Timeline hinzufügt.
7. Custom Data Collector schreiben
Der Symfony Profiler ist erweiterbar: Custom Data Collectors fügen eigene Panels zur Toolbar und zum Profiler hinzu. Das ist nützlich für anwendungsspezifische Metriken — die Anzahl verarbeiteter Business-Events, External-API-Aufrufe mit Latenzen, Feature-Flag-Zustände oder Custom-Cache-Statistiken. Ein Custom Collector implementiert das DataCollectorInterface und definiert welche Daten gesammelt werden, wie sie dargestellt werden und ob sie in der Toolbar erscheinen.
Für Teams, die eigene Services in Symfony-Projekten betreiben, ist ein Custom Collector ein wertvolles Debugging-Werkzeug. Wenn ein API-Client-Service fünf externe HTTP-Aufrufe macht, kann ein Custom Collector jeden Aufruf mit URL, Dauer, HTTP-Status und Response-Größe aufzeichnen. Im Symfony Profiler sieht man dann auf einen Blick, welcher externe Dienst den aktuellen Request verlangsamt. Die Alternative — Logging mit manueller Log-Analyse — ist deutlich zeitaufwändiger. Der Custom Collector speichert seine Daten serialisierbar für die persistierten Profile und macht sie auch für nachträgliche Analyse verfügbar.
<?php
declare(strict_types=1);
namespace App\DataCollector;
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Custom Profiler panel for tracking external API calls.
* Appears as a dedicated panel in the Symfony Profiler.
*/
final class ExternalApiCollector extends AbstractDataCollector
{
/** @var array<array{url: string, duration: float, status: int}> */
private array $calls = [];
/**
* Called by the external API client to record each request.
*/
public function recordCall(string $url, float $durationMs, int $httpStatus): void
{
$this->calls[] = [
'url' => $url,
'duration' => $durationMs,
'status' => $httpStatus,
];
}
/**
* Collect and serialise data when the request finishes.
*/
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
// Data must be serialisable — store in $this->data for Profiler persistence
$this->data = [
'calls' => $this->calls,
'total_calls' => count($this->calls),
'total_duration' => array_sum(array_column($this->calls, 'duration')),
];
}
/** @return array<array{url: string, duration: float, status: int}> */
public function getCalls(): array
{
return $this->data['calls'] ?? [];
}
public function getTotalCalls(): int
{
return $this->data['total_calls'] ?? 0;
}
public function getTotalDuration(): float
{
return $this->data['total_duration'] ?? 0.0;
}
// Name used as panel ID and URL segment in the Profiler
public static function getTemplate(): ?string
{
return '@App/data_collector/external_api.html.twig';
}
}
8. Profiler in PHPUnit-Tests nutzen
Der Symfony Profiler kann in PHPUnit-Tests genutzt werden, um Performance-Regressions automatisch zu erkennen. Wenn ein Feature-Branch plötzlich 50 statt 5 Datenbankabfragen für eine bestimmte Seite produziert, soll das im CI auffallen — nicht erst im Code-Review oder in der Produktion. Der KernelBrowser in Symfony-Tests hat eine enableProfiler()-Methode, die nach dem Request Zugriff auf das Profil ermöglicht. Mit $client->getProfile()->getCollector('db')->getQueryCount() testet man, dass die Anzahl der SQL-Queries unter einem bestimmten Threshold bleibt.
Performance-Tests mit dem Symfony Profiler sind kein Ersatz für Lastests, aber sie fangen die häufigsten Regressions auf Unit-Test-Ebene ab. Ein Test, der sicherstellt, dass die Produktliste mit 100 Produkten maximal 3 SQL-Queries ausführt, verhindert effektiv, dass ein N+1-Problem durch einen unaufmerksamen Commit eingeführt wird. Der Symfony Profiler macht diese Art von Assertions möglich, ohne externe Performance-Monitoring-Tools zu benötigen. In Kombination mit dem Custom-Collector aus dem vorherigen Abschnitt können auch anwendungsspezifische Metriken getestet werden.
9. Symfony Profiler vs. externe APM-Tools
Der Symfony Profiler ist für die Entwicklungsphase das richtige Werkzeug. Für die Produktion braucht man ein ergänzendes Monitoring-System. Die Frage ist, wo die Grenze zwischen Profiler und APM-Tool liegt.
| Kriterium | Symfony Profiler | Datadog / New Relic | Blackfire.io |
|---|---|---|---|
| Einsatzbereich | Entwicklung, Test | Produktion | Dev + Produktion |
| Overhead | Kein Produktions-Overhead | Messbar (1–5%) | Niedrig im Probe-Modus |
| Symfony-Integration | Native, zero config | Agent + PHP-Extension | Symfony-Bundle verfügbar |
| SQL-Analyse | Vollständig mit Stack-Trace | Aggregiert über Zeit | Profil-basiert |
| Kosten | Kostenlos (Open Source) | Kostenpflichtig | Kostenpflichtig ab Produktion |
Die optimale Strategie für Symfony-Projekte kombiniert beide Ansätze: Der Symfony Profiler für die Entwicklungsarbeit und zum Schreiben von Performance-Tests im CI, ein APM-Tool für die Produktionsüberwachung und Alerting bei Performance-Degradationen unter Last. Blackfire.io ist dabei die natürlichste Ergänzung zum Symfony Profiler, weil es von denselben Entwicklern gebaut wird und nahtlos mit dem Symfony-Ökosystem integriert. Für kleinere Projekte ist der Symfony Profiler in Kombination mit strukturiertem Logging und Grafana oft ausreichend — APM-Tools sind eine Investition, die sich erst ab einem gewissen Traffic-Niveau auszahlt.
Mironsoft
Symfony Performance-Analyse, Profiler-Auswertung und Optimierung
Symfony-Performance-Probleme systematisch lösen?
Wir analysieren Symfony-Anwendungen mit dem Profiler, identifizieren N+1-Queries, Cache-Misses und Event-Listener-Bottlenecks und implementieren die Optimierungen für messbaren Performance-Gewinn in eurem Projekt.
Profiler-Analyse
Systematische Auswertung von Symfony-Profiler-Daten und Identifikation von Performance-Bottlenecks
Query-Optimierung
N+1-Queries mit Doctrine Eager Loading und DQL-Optimierungen beheben
Performance-Tests
PHPUnit-Tests mit Profiler-Assertions schreiben, die Performance-Regressions in der CI verhindern
10. Zusammenfassung
Der Symfony Profiler ist das effektivste Diagnosewerkzeug für Performance-Arbeit in Symfony-Anwendungen. Das Doctrine-Panel deckt N+1-Queries mit Stack-Traces auf und zeigt genau, welcher Code-Pfad wie viele SQL-Abfragen auslöst. Das Events-Panel macht langsame Listener sichtbar. Das Cache-Panel zeigt Hit/Miss-Verhältnisse für alle Cache-Pools. Die Timeline visualisiert den gesamten Request-Lifecycle und macht Bottlenecks auf Anhieb sichtbar. Custom Data Collectors erweitern den Profiler um anwendungsspezifische Metriken.
Der größte Return on Investment liegt darin, den Symfony Profiler nicht nur reaktiv zu nutzen — wenn eine Seite langsam ist — sondern proaktiv als Teil des Entwicklungsprozesses. PHPUnit-Tests, die Query-Counts über den Profiler-API prüfen, verhindern Performance-Regressions in der CI, bevor sie die Produktion erreichen. Kombiniert mit dem Stopwatch-Service für Custom-Timing und einem APM-Tool für die Produktionsüberwachung entsteht eine vollständige Performance-Observability-Strategie, die in Symfony-Projekten ohne externe Werkzeuge machbar ist.
Symfony Profiler — Das Wichtigste auf einen Blick
N+1-Queries finden
Doctrine-Panel zeigt alle SQL-Queries mit Dauer und Stack-Trace. Mehrfach identische Queries mit unterschiedlichen IDs = N+1-Problem. Fix: Eager Loading via leftJoin + addSelect.
Timeline-Analyse
Timeline zeigt Request-Lifecycle als Zeitleiste. Großer Controller-Balken = Arbeit im Service. Stopwatch-Service für Custom-Spans im Timeline nutzen.
Custom Collector
DataCollectorInterface implementieren für eigene Panels. Anwendungsspezifische Metriken wie External-API-Calls im Profiler sichtbar machen.
Performance-Tests
$client->enableProfiler() in PHPUnit. Query-Count-Assertions verhindern N+1-Regressions in der CI. getCollector('db')->getQueryCount().