Symfony Asset-Versionierung und Cache-Busting in Produktion
AI generated
SF
{ }
Symfony · Performance · Deployment
Asset-Versionierung und Cache-Busting
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.

17 Min. Lesezeit AssetMapper json_manifest_path

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.

11. FAQ: Asset-Versionierung: Das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen json_manifest_path und AssetMapper?
json_manifest_path liest eine manifest.json, die ein externes Build-Tool wie Webpack Encore erzeugt hat, während AssetMapper ein eigenständiger, in Symfony integrierter Mechanismus ohne Node-Build-Prozess ist. Beide erzeugen am Ende denselben Effekt: eindeutige, content-hash-basierte Dateinamen, die von der Twig-Funktion asset() aufgelöst werden.
2Ist Query-String-Versionierung grundsätzlich schlecht?
Nein, für kleine Projekte ohne CDN und ohne mehrere App-Server ist sie eine praktikable, einfache Lösung. Sobald jedoch ein CDN oder mehrere Reverse-Proxys mit eigener Caching-Logik im Spiel sind, steigt das Risiko, dass Query-Strings beim Caching ignoriert werden und alte Versionen ausgeliefert bleiben.
3Was bewirkt Cache-Control: immutable konkret im Browser?
Es weist den Browser an, selbst bei einem manuellen Reload keine bedingte Anfrage mit If-None-Match oder If-Modified-Since mehr an den Server zu senden, solange der lokale Eintrag noch innerhalb der max-age liegt. Das spart eine komplette Round-Trip-Zeit pro Asset und wirkt sich besonders auf mobilen Verbindungen spürbar aus.
4Muss ich Webpack Encore weiter nutzen, wenn ich auf AssetMapper umsteige?
Nein, beide Systeme sind Alternativen und lassen sich in der Regel nicht sinnvoll parallel im selben Projekt betreiben. Für Projekte mit komplexem JavaScript-Bundling bleibt Encore jedoch oft die praktischere Wahl, während AssetMapper für schlankere Stimulus-basierte Frontends ausreicht.
5Wie vermeide ich 404-Fehler bei Multi-Server-Deployments zuverlässig?
Am zuverlässigsten ist es, Assets nur einmal zentral in der CI-Pipeline zu bauen und das identische Artefakt an alle Server oder in einen gemeinsamen Objektspeicher hinter einem CDN zu verteilen, statt jeden Server unabhängig bauen zu lassen. Zusätzlich sollte das Umschalten auf die neue Version atomar erfolgen.
6Wie lange sollte ich alte Asset-Builds nach einem Deployment vorhalten?
Als grobe Faustregel gelten zwei bis drei vorherige Releases oder mindestens die maximale Cache-Zeit des HTML-Dokuments, damit Nutzer mit einer im Cache liegenden älteren Seite noch auf die passenden Asset-Hashes zugreifen können, bevor der alte Build entfernt wird.
7Darf die manifest.json selbst lange gecacht werden?
Nein, sie muss bei jedem Deployment aktuell sein, weil sie die Zuordnung von logischem zu physischem, gehashtem Dateinamen enthält. Für diese Datei ist no-cache mit must-revalidate die richtige Konfiguration, sowohl am Ursprungsserver als auch am CDN.
8Funktioniert CDN-Caching automatisch korrekt mit AssetMapper?
Grundsätzlich ja, solange die Cache-Regeln am CDN explizit auf gehashte Pfade unter public/assets/ zielen und nicht versehentlich das gesamte Verzeichnis inklusive eventueller Manifest- oder Debug-Endpunkte pauschal einschließen.
9Wie teste ich Cache-Busting lokal, bevor es live geht?
Am einfachsten mit bin/console asset-map:compile beziehungsweise dem Encore-Production-Build lokal ausführen, die Response-Header der erzeugten Dateien mit curl -I prüfen und anschließend eine Datei absichtlich ändern, um zu verifizieren, dass sich tatsächlich der Hash und damit die URL ändert.
10Lohnt sich AssetMapper auch für größere, JavaScript-lastige Anwendungen?
Das kommt auf die Komplexität der Abhängigkeiten an. Für Projekte, die primär ES-Module ohne aufwendiges Bundling nutzen, funktioniert AssetMapper gut. Sobald jedoch viele npm-Pakete mit komplexen Build-Schritten wie TypeScript-Kompilierung oder JSX-Transformation nötig sind, ist Webpack Encore meist die robustere Wahl.