Vite für Symfony: Webpack Encore ablösen, ohne die Asset-Pipeline zu brechen

#Webpack Encore zu Vite Migration
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Webpack Encore war lange die Standard-Asset-Pipeline in Symfony-Projekten, und es funktioniert noch. Das Symfony-Team pflegt es, deine Builds laufen durch, nichts brennt. Aber das Ökosystem drumherum hat sich bewegt. Neue Symfony-7-Projekte starten mit AssetMapper oder Vite, webpack selbst wird kaum noch aktiv weiterentwickelt, und fast jedes Frontend-Tooling seit 2024 setzt native ES Modules voraus. Wenn deine Anwendung bei jedem Build noch Encore hochfährt, lohnt es sich, eine Webpack Encore zu Vite Migration zu planen, und sie ist weniger disruptiv, als die meisten Teams erwarten. Dieser Beitrag zeigt, was sich wirklich ändert, die drei Migrationsansätze, die wir in Kundenprojekten nutzen, die Encore-Settings ohne direktes Vite-Äquivalent und die Build-Zeiten, die wir vor und nach einer echten Migration gemessen haben.

Was du konkret gewinnst

Vite trennt Development und Production in zwei getrennte Pipelines. In der Entwicklung liefert es deine Quelldateien als native ES Modules aus und transformiert nur, was der Browser anfordert, es gibt also gar kein Bundling im Voraus. Der Dev-Server startet auch bei großen Projekten in deutlich unter einer Sekunde, und Hot Module Replacement spielt eine CSS- oder Komponentenänderung fast sofort ein, weil Vite nur die Datei neu transformiert, die du angefasst hast. Production-Builds laufen durch Rollup und erzeugen gehashte, tree-shaked Bundles, ganz ähnlich wie Encore heute.

Für ein Symfony-Team zählen im Alltag vor allem drei Unterschiede: die Startzeit des Dev-Servers, HMR-Geschwindigkeit, die auch bei wachsendem Projekt konstant bleibt, und eine Konfigurationsdatei, die nur einen Bruchteil so groß ist. Das meiste, was webpack.config.js von Hand verdrahtet, Loader, Dev-Middleware, Code Splitting, ist in Vite Default-Verhalten.

Sei ehrlich zu deinem Team, was du nicht gewinnst. Vite überspringt beim Transformieren das Type Checking komplett, langsames TypeScript-Feedback bleibt also langsam, solange du tsc nicht separat laufen lässt, was du in der CI ohnehin tun solltest. Und die Production-Build-Zeiten verbessern sich, aber deutlich weniger dramatisch als der Feedback-Loop in der Entwicklung.

Die zwei Pakete, die Encore ersetzen

Auf der Symfony-Seite wird symfony/webpack-encore-bundle durch ein Paar ersetzt:

  • vite-plugin-symfony, ein npm-Paket, das Vite für Symfonys Verzeichnisstruktur konfiguriert und eine entrypoints.json schreibt, die die Twig-Seite lesen kann
  • pentatrion/vite-bundle, ein Composer-Paket mit vite_entry_script_tags() und vite_entry_link_tags(), den direkten Gegenstücken zu encore_entry_script_tags() und encore_entry_link_tags()

Eine minimale vite.config.js für ein Symfony-Projekt sieht so aus:

import { defineConfig } from 'vite';
import symfonyPlugin from 'vite-plugin-symfony';

export default defineConfig({
  plugins: [symfonyPlugin()],
  build: {
    rollupOptions: {
      input: {
        app: './assets/app.js',
        admin: './assets/admin.js',
      },
    },
  },
});

Vergleich das mit einer typischen webpack.config.js voller Encore-Chained-Calls für Entries, Sass, PostCSS, Source Maps, Versioning und Runtime-Config, und die Reduktion ist offensichtlich. Entries mappen eins zu eins: jedes Encore.addEntry() wird ein Key unter rollupOptions.input.

In Twig ist die Änderung mechanisch:

{# vorher #}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }}

{# nachher #}
{{ vite_entry_link_tags('app') }}
{{ vite_entry_script_tags('app') }}

Wenn du Stimulus und Symfony UX nutzt: vite-plugin-symfony bringt eine Stimulus-Bridge mit, deine Controller funktionieren also weiter. Plane eine Stunde ein, um Lazy Controller zu prüfen, denn deren Ladeverhalten hängt vom Bundler ab.

Drei Ansätze für eine Webpack Encore zu Vite Migration

Welcher Weg passt, hängt davon ab, wie viel Frontend-Code du hast und wie viel Risiko das Team in einem Release tragen kann.

Der erste Ansatz ist der komplette Austausch in einem einzigen Pull Request. Encore entfernen, beide Pakete oben installieren, die Config portieren, die Twig-Helper tauschen, webpack.config.js löschen. Für Anwendungen mit einer Handvoll Entry Points und konventionellem Setup (Sass, PostCSS, etwas TypeScript) sind das ein bis zwei Tage Arbeit inklusive Testen. Für die meisten Projekte ist das der richtige Weg, denn zwei Bundler parallel zu betreiben hat eigene laufende Kosten.

Der zweite Ansatz ist ein Hybrid mit AssetMapper. Symfonys AssetMapper liefert einfache Assets ohne Build-Schritt aus: er mappt Dateien, versioniert sie und schreibt eine Importmap. Wenn große Teile deines Asset-Verzeichnisses reines CSS und Vanilla-JS sind, die nur durch Encore liefen, weil alles durchlaufen musste, verschieb sie zu AssetMapper und reserviere Vite für den Code, der Kompilierung braucht, etwa TypeScript, JSX oder Sass. Du bekommst zwei kleine, zweckgebundene Pipelines statt einer großen. Der Preis: dein Team muss wissen, welches Asset wo lebt, dokumentiere die Aufteilung also im Repo.

Der dritte Ansatz ist inkrementell, ein Entry Point nach dem anderen. Encore und das Vite-Bundle können koexistieren: beide schreiben in public/build unter verschiedene Unterverzeichnisse, und jedes Twig-Layout ruft den Helper auf, der zu seinem Entry passt. Migriere in diesem Sprint das Admin-Bundle, im nächsten den Checkout-Flow, und liefere währenddessen weiter Features. Das ist der sicherste Weg für große Anwendungen mit vielen Entry Points und schwerer webpack-Anpassung. Setz dir aber eine Deadline für den Abschluss. Jede Woche, in der beide Bundler existieren, laufen in der CI beide Builds, und jedes Dependency-Upgrade muss zwei Toolchains zufriedenstellen.

Die Encore-Settings ohne direktes Äquivalent

Der Großteil der Migration ist Umbenennen. Diese vier Bereiche brauchen echtes Nachdenken.

Encores copyFiles() hat kein Gegenstück in Vites Config. Vite erwartet, dass statische Dateien entweder in einem public-Verzeichnis liegen, das unverändert kopiert wird, oder aus Quelldateien importiert werden, damit sie in den Dependency-Graph gelangen und gehasht werden. Für die meisten Projekte ist die Lösung, kopierte Dateien (Icons, Legacy-Vendor-Skripte, robots-Ausschlüsse) ins public-Verzeichnis zu verschieben. Wenn du transformierte Kopien brauchst, füllt rollup-plugin-copy die Lücke.

Environment-Variablen werden per Allowlist injiziert. Encore-Setups nutzen oft configureDefinePlugin(), um beliebige Werte zur Build-Zeit zu inlinen. Vite stellt über import.meta.env nur Variablen mit dem Präfix VITE_ bereit, alles andere ist für Client-Code unsichtbar. Das ist ein besserer Default, weil es schwerer wird, ein serverseitiges Secret in ein Bundle zu leaken, aber es heißt, dass du die Variablen umbenennen musst, die dein Frontend wirklich liest, und auditieren solltest, was vorher inlined wurde. In zwei der letzten drei Migrationen, die wir gemacht haben, förderte dieses Audit Werte im Bundle zutage, die dort nie hätten landen dürfen. Betrachte das Audit als Feature der Migration, nicht als lästige Pflicht.

autoProvidejQuery() ist weg, und es gibt keinen höflichen Ersatz. Vite reicht nicht stillschweigend ein globales $ an jedes Modul weiter. Entweder fügst du explizite jquery-Imports in den Dateien hinzu, die es nutzen, was ein Codemod schnell erledigt, oder du verdrahtest @rollup/plugin-inject, um das alte Verhalten nachzubauen. Nimm die expliziten Imports, wenn du kannst. Das magische Global ist genau die Art versteckter Kopplung, die die nächste Migration schwerer macht.

PostCSS dagegen zieht ohne Änderungen um. Vite liest postcss.config.js nativ, Tailwind, autoprefixer und Co. funktionieren also in dem Moment, in dem die Datei existiert. Fonts und Bilder, die aus CSS referenziert werden, werden ebenfalls automatisch behandelt, inklusive Hashing.

Die CI-Änderung

Die Deployment-Story ändert sich kaum, und genau das ist der Punkt des Bundle-Paars. Dein CI-Schritt wechselt von Encores Production-Build zu vite build. Der Output landet weiterhin in public/build, Dateinamen sind weiterhin content-gehasht, und die Entrypoints-Datei sagt Twig weiterhin, was einzubinden ist. Deine Webserver-Config, dein CDN-Setup und deine Deploy-Skripte bleiben, wie sie sind. Prüfen solltest du nur deine CI-Cache-Konfiguration: Vites Cache für das Dependency-Pre-Bundling liegt in node_modules/.vite, und ihn zu cachen verkürzt den Install-und-Build-Schritt weiter.

Die Zahlen aus einer echten Migration

Wir haben in diesem Frühjahr eine mittelgroße B2B-Symfony-Anwendung migriert: sechs Entry Points, TypeScript plus Sass, Stimulus-Controller, rund 900 Module im Graph. Gleicher CI-Runner vorher und nachher. Der Production-Build ging von 84 Sekunden mit Encore auf 31 Sekunden mit Vite. Der Dev-Server, der unter webpack etwa 20 Sekunden brauchte, bis er nutzbar war, startet jetzt in unter einer Sekunde. HMR ging von ein bis drei Sekunden pro Änderung auf praktisch sofort. Beim Testen tauchten keine visuellen Regressionen auf; das einzige Laufzeitproblem war ein Legacy-Skript, das sich auf das injizierte jQuery-Global verließ, gefunden im Staging.

Das Fazit des Teams nach zwei Wochen: niemand wollte je wieder ein webpack-Projekt anfassen. Schnelleres Feedback verstärkt sich selbst: Entwickler laden seltener neu, sammeln Änderungen weniger und vertrauen der Dev-Umgebung mehr.

Wann es vertretbar ist, bei Encore zu bleiben

Build-Tooling zu migrieren ist echte Arbeit, und nicht jedes Projekt sollte das jetzt tun. Wenn die Anwendung im Wartungsmodus ist und einmal im Quartal eine Frontend-Änderung sieht, wird Encore sie noch Jahre bauen. Wenn dein Setup stark auf webpack-spezifische Plugins ohne Rollup-Äquivalent setzt, rechne den Aufwand für deren Ersatz ein, bevor du dich festlegst. Und wenn dein Frontend so einfach ist, dass es gar keinen Build-Schritt braucht, ist der Sprung direkt zu AssetMapper unter Auslassung von Vite noch besser, denn er entfernt die Node-Toolchain komplett aus dem Projekt.

Wovon wir abraten, ist die Entscheidung per Trägheit. Encores Dependency-Baum altert, webpack 5 bekommt Patches, aber wenig mehr, und mit jedem Jahr, das die Migration wartet, driftet das umliegende Ökosystem weiter Richtung ES Modules. Asset-Pipelines sind die Art Infrastruktur, die sich nach deinem Zeitplan günstig modernisieren lässt und unter Druck teuer wird.

Wenn du diese Migration als Teil eines größeren Modernisierungsvorhabens abwägst: das ist Arbeit, die wir regelmäßig machen, von Legacy-Stack-Assessments bis zu Tech-Stack-Entscheidungen für Teams, die die nächsten drei Jahre planen. Schreib an hello@wolf-tech.io oder schau dich auf wolf-tech.io um, und wir sagen dir ziemlich schnell, ob für deine Codebasis ein Austausch in einem PR oder ein inkrementeller Pfad passt.