in der Symfony-Produktion
Lange Cache-Zeiten für CSS und JavaScript sind gut für die Performance, aber gefährlich, wenn sich der Inhalt einer Datei ändert, ihre URL jedoch gleich bleibt. Dieser Artikel zeigt, warum content-hash-basierte Versionierung über json_manifest_path oder AssetMapper der Query-String-Variante vorzuziehen ist, wie Nginx und CDN korrekt konfiguriert werden und welche Fallstricke bei Multi-Server-Deployments ohne geteiltes Manifest lauern.
Inhaltsverzeichnis
- 1. Warum Cache-Busting bei Assets überhaupt nötig ist
- 2. json_manifest_path vs. Query-String-Versionierung
- 3. Wie AssetMapper Content-Hashes für Cache-Busting nutzt
- 4. Webpack Encore als Alternative für komplexe Frontends
- 5. Nginx- und Caching-Header für versionierte Assets korrekt konfigurieren
- 6. CDN-Konfiguration und Invalidierung
- 7. Fallstricke bei Multi-Server-Deployments ohne geteiltes Manifest
- 8. Zentrales Build-Artefakt und atomare Deployment-Strategien
- 9. Monitoring und Fazit
- 10. Zusammenfassung
- 11. FAQ
1. Warum Cache-Busting bei Assets überhaupt nötig ist
Browser und CDNs cachen statische Assets wie CSS- und JavaScript-Dateien standardmäßig sehr aggressiv, sobald der Server einen langen max-age-Wert im Cache-Control-Header sendet, weil wiederkehrende Downloads unnötig Bandbreite und Ladezeit kosten. Das Problem entsteht in dem Moment, in dem ein Deployment den Inhalt einer solchen Datei ändert, die URL im Browser-Cache aber unverändert bleibt: Der Browser liefert dann munter die veraltete Version aus dem Cache aus, obwohl der Server längst eine neue Version bereithält, und der Nutzer sieht im schlimmsten Fall eine defekte Seite, weil altes CSS mit neuem HTML kollidiert. Ein hartes Cache-Control: no-cache für alle Assets wäre zwar sicher, würde aber jeden Performance-Vorteil des Browser-Caches zunichtemachen und ist deshalb in der Praxis keine Option.
Cache-Busting löst dieses Dilemma, indem es die URL einer Datei ändert, sobald sich ihr Inhalt ändert, wodurch unveränderte Dateien weiterhin beliebig lange gecacht werden können, während geänderte Dateien automatisch unter einer neuen Adresse abgerufen werden. Symfony bietet dafür über die Asset-Component mehrere Versionierungsstrategien an, von einer simplen globalen Versionsnummer bis zu einem Manifest, das jede Datei individuell mit einem aus ihrem Inhalt berechneten Hash versieht. Welche Strategie zum Einsatz kommt, entscheidet in Produktion maßgeblich darüber, ob Cache-Busting zuverlässig funktioniert oder in bestimmten Konstellationen, etwa hinter einem CDN oder auf mehreren App-Servern, unbemerkt versagt.
2. json_manifest_path vs. Query-String-Versionierung
Die einfachste Variante ist die Query-String-Versionierung über framework.assets.version, bei der Symfony an jede Asset-URL einen Parameter wie ?v1 anhängt und diesen bei jeder Änderung der Versionsnummer manuell oder über eine Umgebungsvariable erhöht. Der Vorteil liegt in der Einfachheit: kein Build-Schritt, keine Manifest-Datei, nur eine einzige Konfigurationszeile. Der Nachteil zeigt sich in Produktion mit CDN oder mehreren Reverse-Proxys davor, weil nicht jede Caching-Schicht Query-Strings als Teil des Cache-Keys behandelt. Manche Konfigurationen ignorieren Query-Parameter bewusst, um die Trefferquote des Caches zu erhöhen, was in diesem Fall dazu führt, dass trotz geänderter Versionsnummer die alte, gecachte Antwort ausgeliefert wird. Außerdem ändert sich bei diesem Ansatz die Versionsnummer meist global für alle Assets gleichzeitig, sodass auch unveränderte Dateien beim nächsten Deployment neu heruntergeladen werden müssen.
json_manifest_path verfolgt einen präziseren Ansatz: Ein Build-Tool wie Webpack Encore erzeugt beim Kompilieren eine manifest.json, die jeden logischen Dateinamen auf eine physische, mit einem Content-Hash versehene Datei abbildet, etwa app.css auf app.3f5e9c2b.css. Die Twig-Funktion asset() liest dieses Manifest zur Laufzeit aus und gibt automatisch den korrekten, gehashten Pfad zurück. Ändert sich der Inhalt einer Datei nicht, bleibt auch ihr Hash und damit ihre URL identisch, weshalb sich nur tatsächlich geänderte Dateien neu invalidieren, während unveränderte Dateien beliebig lange im Cache bleiben dürfen. Dieser Mechanismus ist robuster gegenüber Caching-Schichten, die Query-Strings ignorieren, weil der Pfad selbst und nicht nur ein Parameter dahinter die Änderung trägt.
# config/packages/framework.yaml
framework:
assets:
# Variante A: einfache, globale Query-String-Versionierung
# version: 'v%env(APP_VERSION)%'
# version_format: '%%s?v=%%s'
# Variante B: Manifest-basierte Versionierung pro Datei (empfohlen)
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
3. Wie AssetMapper Content-Hashes für Cache-Busting nutzt
AssetMapper ist Symfonys nativer Asset-Pipeline-Ansatz ohne Node-basierten Build-Prozess, seit Symfony 6.4 stabil und für Projekte gedacht, die keine komplexe JavaScript-Bundling-Kette wie Webpack benötigen. Assets liegen in einem Verzeichnis wie assets/, werden über eine importmap.php referenziert und beim Aufruf von bin/console asset-map:compile für die Produktion in das öffentliche Verzeichnis public/assets/ geschrieben, wobei jede Datei einen aus ihrem Inhalt berechneten Hash im Dateinamen erhält, etwa app-3f5e9c2b8a1d.js. Intern führt AssetMapper dabei ein eigenes Mapping von logischem zu physischem Pfad, sodass die Twig-Funktion asset() denselben Mechanismus wie bei json_manifest_path nutzt, nur ohne dass ein externes Build-Tool eine manifest.json erzeugen muss.
Im Entwicklungsmodus liefert AssetMapper Dateien dagegen unverändert und ungehasht direkt aus, damit Änderungen am Code sofort ohne Kompilierschritt sichtbar sind, was den Entwickler-Workflow deutlich beschleunigt. Erst der explizite Compile-Schritt vor dem Produktions-Deployment erzeugt die versionierten, cachefähigen Dateien. Wichtig ist dabei, dass asset-map:compile deterministisch denselben Hash für identischen Inhalt erzeugt, unabhängig davon, auf welchem Server oder zu welchem Zeitpunkt der Befehl läuft, was die Grundlage dafür ist, dass mehrere App-Server bei synchronem Build zum selben Ergebnis kommen.
4. Webpack Encore als Alternative für komplexe Frontends
Für Projekte mit umfangreicher JavaScript-Logik, etwa eingebetteten Vue- oder React-Komponenten, npm-Abhängigkeiten mit eigener Build-Kette oder SCSS-Präprozessoren, bleibt Webpack Encore die passendere Wahl gegenüber AssetMapper, weil es echtes Bundling, Tree-Shaking und Code-Splitting mitbringt. Encore erzeugt über Encore.enableVersioning() dieselbe manifest.json, die dann per json_manifest_path in Symfony eingebunden wird, und kann zusätzlich mit enableIntegrityHashes() Subresource-Integrity-Hashes für zusätzliche Sicherheit gegen manipulierte CDN-Auslieferung generieren. Für Projekte mit mehreren Einstiegspunkten schreibt Encore außerdem eine entrypoints.json, über die Symfony automatisch alle zu einem Entry-Point gehörenden CSS- und JS-Dateien in der richtigen Reihenfolge einbindet.
Die Entscheidung zwischen AssetMapper und Webpack Encore ist keine reine Geschmacksfrage, sondern hängt vom tatsächlichen Frontend-Bedarf ab: Wer überwiegend mit Stimulus-Controllern und wenig externem npm-Code arbeitet, fährt mit AssetMapper meist einfacher und schneller, weil kein Node-Toolchain gepflegt werden muss. Wer dagegen auf ein umfangreiches JavaScript-Ökosystem mit vielen Abhängigkeiten setzt, profitiert von Encores ausgereifterem Bundling. Beide Ansätze teilen sich am Ende dasselbe Grundprinzip: Content-Hash im Dateinamen statt Query-String, damit Cache-Busting zuverlässig funktioniert.
5. Nginx- und Caching-Header für versionierte Assets korrekt konfigurieren
Sobald Assets über einen Content-Hash im Dateinamen eindeutig identifiziert sind, kann Nginx für diese Pfade das aggressivste sinnvolle Caching-Regime setzen: Cache-Control: public, max-age=31536000, immutable. Das immutable-Directive weist Browser explizit an, selbst bei einem Reload keine bedingte Anfrage mehr an den Server zu stellen, weil sich der Inhalt unter dieser URL per Definition nie ändert, was gerade auf langsamen Mobilfunkverbindungen spürbar Ladezeit spart. Wichtig ist, diese Regel nur auf tatsächlich gehashte Pfade anzuwenden und nicht versehentlich auf das HTML-Dokument selbst, das ja bei jedem Deployment einen neuen Verweis auf die aktuellen Asset-Hashes enthalten muss.
Die manifest.json beziehungsweise der AssetMapper-interne Manifest-Endpunkt dürfen dagegen nicht lange gecacht werden, weil sie genau die aktuelle Zuordnung von logischem zu physischem Pfad enthalten und bei jedem Deployment aktualisiert werden. Für diese Datei ist no-cache mit must-revalidate die richtige Wahl, damit jede Anfrage tatsächlich beim Ursprungsserver landet und nicht versehentlich eine veraltete Zuordnung ausliefert. In der Praxis bewährt sich eine Nginx-Konfiguration mit zwei klar getrennten location-Blöcken, einer für gehashte Asset-Pfade mit langem Cache und einer für alles andere mit kurzer oder keiner Caching-Zeit.
# /etc/nginx/conf.d/symfony-assets.conf
location ~* ^/(build|assets)/.+\.[0-9a-f]{8,}\.(css|js|woff2?|png|jpe?g|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
try_files $uri =404;
}
location = /build/manifest.json {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
6. CDN-Konfiguration und Invalidierung
Vor einem CDN wie Cloudflare oder Fastly gilt derselbe Grundsatz wie bei Nginx: gehashte Asset-Pfade dürfen ohne Bedenken mit maximaler Cache-Zeit an der Edge vorgehalten werden, weil eine neue Version automatisch unter einer neuen URL erscheint und niemals manuell purged werden muss. Genau das ist der eigentliche Vorteil gegenüber Query-String-Versionierung, bei der viele CDN-Konfigurationen mit aktivierter Option zum Ignorieren von Query-Strings faktisch nie neu validieren, sodass ein manuelles Purgen nach jedem Deployment nötig würde, was in automatisierten Pipelines leicht vergessen wird und zu tagelang veralteten Assets führen kann.
Für die manifest.json und alle HTML-Antworten sollte am CDN dieselbe kurze oder deaktivierte Caching-Regel greifen wie am Ursprungsserver, damit Nutzer nach einem Deployment sofort die neue Version des HTML-Dokuments mit den korrekt referenzierten, neuen Asset-Hashes erhalten. Ein häufiger Fehler ist, am CDN eine generelle Page-Rule für das gesamte Verzeichnis /build/ zu setzen, die versehentlich auch die manifest.json mit einschließt. Deshalb lohnt es sich, Cache-Regeln explizit über Dateiendungen oder ein Hash-Pattern im Pfad zu definieren statt über ein pauschales Verzeichnis-Präfix.
7. Fallstricke bei Multi-Server-Deployments ohne geteiltes Manifest
Sobald eine Anwendung auf mehreren App-Servern hinter einem Load Balancer läuft und jeder Server seine Assets lokal und unabhängig baut, entsteht ein subtiles, aber gravierendes Problem: Wird während eines rollierenden Deployments zuerst Server A aktualisiert und liefert Server A bereits HTML mit Verweisen auf neue Asset-Hashes aus, kann eine nachfolgende Anfrage für genau diese Datei vom Load Balancer an Server B geroutet werden, der noch den alten Build besitzt und die neue Datei schlicht nicht kennt. Das Ergebnis ist ein 404-Fehler für CSS oder JavaScript mitten im Live-Betrieb, obwohl jeder einzelne Server für sich genommen korrekt konfiguriert ist.
Das umgekehrte Problem tritt auf, wenn Deployment-Skripte alte Asset-Verzeichnisse sofort nach dem Umschalten löschen, um Speicherplatz zu sparen: Browser-Tabs, die vor dem Deployment geöffnet wurden oder deren HTML-Seite noch aus dem Browser-Cache stammt, referenzieren dann Hashes, die es auf dem Server nicht mehr gibt, was ebenfalls zu 404-Fehlern führt, diesmal für längst aktive Nutzersitzungen. Beide Szenarien haben eine gemeinsame Ursache: ein fehlendes, zentral geteiltes Manifest beziehungsweise ein fehlender zentral geteilter Asset-Bestand über alle Server hinweg.
8. Zentrales Build-Artefakt und atomare Deployment-Strategien
Die robuste Lösung besteht darin, Assets genau einmal in der CI-Pipeline zu bauen und das resultierende Artefakt, inklusive manifest.json beziehungsweise dem AssetMapper-Ausgabeverzeichnis, identisch an alle App-Server oder direkt in einen zentralen Objektspeicher wie S3 hinter einem CDN zu verteilen, statt jeden Server unabhängig und potenziell zeitversetzt bauen zu lassen. Ein atomares Deployment-Muster mit einem releases/-Verzeichnis und einem current-Symlink, wie es Tools nach dem Capistrano-Vorbild umsetzen, stellt zusätzlich sicher, dass der Wechsel auf eine neue Version für alle Server praktisch gleichzeitig erfolgt und nicht schrittweise über mehrere Minuten hinweg.
Ergänzend hat es sich bewährt, die letzten zwei bis drei Releases samt ihrer Assets nicht sofort zu löschen, sondern eine Übergangszeit vorzuhalten, damit Nutzer mit einer im Browser-Cache liegenden älteren HTML-Seite weiterhin auf die zugehörigen, ebenfalls noch vorhandenen Asset-Hashes zugreifen können. Erst nachdem sicher ist, dass keine aktive Sitzung mehr auf einen alten Release verweist, etwa nach Ablauf der maximalen HTML-Cache-Zeit, kann der alte Build gefahrlos entfernt werden. Diese kleine Verzögerung beim Aufräumen verhindert die meisten der in der Praxis beobachteten Asset-404-Fehler nach Deployments zuverlässig.
9. Monitoring und Fazit
Ein einfacher, aber wirkungsvoller Schutzmechanismus ist die Überwachung der 404-Rate für Pfade unter /build/ oder /assets/ direkt nach jedem Deployment, etwa über ein Dashboard auf Basis der Nginx- oder CDN-Access-Logs. Ein plötzlicher Anstieg unmittelbar nach einem Rollout ist ein zuverlässiges Frühwarnsignal für genau die Multi-Server- oder Aufräum-Probleme, die weiter oben beschrieben wurden, und erlaubt es, den Rollback deutlich früher auszulösen als wenn man auf Nutzerbeschwerden wartet.
Zusammengefasst gilt für Produktion die klare Empfehlung, Content-Hash-basierte Versionierung über json_manifest_path oder AssetMapper einzusetzen statt einer globalen Query-String-Version, weil sie mit CDNs und Reverse-Proxys zuverlässiger zusammenarbeitet und präzises, dateiweises Cache-Busting ermöglicht. Diese Versionierung entfaltet ihr volles Potenzial aber erst in Kombination mit korrekt getrennten Caching-Headern für gehashte Assets und Manifest sowie einer Deployment-Strategie, die Assets zentral baut und Server synchron aktualisiert, statt jeden Knoten unabhängig voneinander bauen zu lassen.
| Aspekt | Query-String-Versionierung | Manifest/Content-Hash | AssetMapper (nativ) |
|---|---|---|---|
| Cache-Busting-Mechanismus | ?v=... Parameter an der URL | Hash im Dateinamen | Hash im Dateinamen unter public/assets/ |
| CDN-Kompatibilität | Fehleranfällig bei ignorierten Query-Strings | Robust, da eindeutiger Pfad | Robust, da eindeutiger Pfad |
| immutable-Caching sinnvoll | Nur eingeschränkt empfehlenswert | Ja, uneingeschränkt | Ja, uneingeschränkt |
| Build-Tool erforderlich | Nein | Ja, z. B. Webpack Encore | Nein, nativ ab Symfony 6.4 |
| Risiko bei Multi-Server-Deployment | Gering | Hoch ohne geteiltes Manifest | Hoch ohne geteilten Output |
Mironsoft
Symfony-Architektur, saubere Domänenlogik und Legacy-Modernisierung
Symfony-Anwendungen, die nach zwei Jahren noch wartbar sind?
Wir prüfen bestehende Symfony-Projekte auf aufgeblähte Controller, fehlende Serviceabstraktionen und ungetestete Kernlogik und bauen daraus eine Architektur, die neue Features aufnimmt, ohne bei jedem Release brüchiger zu werden.
Architektur-Review
Bundle-Struktur, Dependency Injection und Serviceabstraktionen auf Wartbarkeit prüfen.
Legacy-Modernisierung
Schrittweise Migration veralteter Symfony-Versionen ohne kompletten Neuschrieb.
Test- und Qualitätssicherung
PHPUnit, PHPStan und CI-Pipelines für nachhaltige Codequalität aufbauen.
10. Zusammenfassung
Asset-Versionierung: Das Wichtigste auf einen Blick
Kernproblem
Statische Assets bleiben im Browser-Cache, obwohl sich ihr Inhalt nach einem Deployment geändert hat, wenn die URL gleich bleibt.
Empfohlene Lösung
Content-Hash-basierte Versionierung via json_manifest_path oder AssetMapper kombiniert mit immutable Cache-Control.
Multi-Server-Fallstrick
Unsynchronisierte Manifeste oder zu früh gelöschte Builds führen während des Rollouts zu 404-Fehlern.
Deployment-Empfehlung
Assets zentral einmal bauen, an alle Server identisch verteilen, alte Releases kurz vorhalten, atomar umschalten.