Was wäre, wenn dein Frontend-Build-Prozess einfach… nichts wäre?
Die Rückkehr zur Einfachheit in der Frontend-Entwicklung
Ich habe die letzten fünf Jahre damit verbracht, Webpack-Konfigurationen zu
pflegen. Fünf Jahre, in denen npm install drei Minuten gedauert hat. Fünf
Jahre Debugging, warum mein Build plötzlich nicht mehr funktioniert, nachdem ich
ein einziges Package aktualisiert habe. Fünf Jahre, in denen ich
Junior-Entwicklern erklären musste, warum sie ein komplett separates Ökosystem
lernen müssen, nur um eine JavaScript-Datei zu einem Symfony-Projekt
hinzuzufügen.
Dann kam Symfony 6.3 mit AssetMapper, und mir wurde klar: Wir hatten die ganze Zeit das falsche Problem gelöst.
Das Versprechen klingt fast zu gut, um wahr zu sein: Schreib modernes JavaScript und CSS ohne jegliches Build-System. Kein Node.js, kein npm, kein Webpack, keine Konfigurationsdateien, keine Transpilierungs-Pipeline. Nur PHP und die nativen Features, die Browser seit Jahren unterstützen.
Ich war skeptisch. Solltest du auch sein. Aber nachdem ich drei Produktionsanwendungen migriert habe, bin ich überzeugt: Das ist die Zukunft der Symfony-Frontend-Entwicklung.
Das Problem, das wir uns selbst geschaffen haben
Kennste das? Du startest ein neues Symfony-Projekt:
symfony new mein-projekt --webapp
In den “alten Zeiten” (also… 2023) hast du sofort als nächstes ausgeführt:
composer require symfony/webpack-encore-bundle
npm install
Und dann musstest du:
- Node.js installieren (natürlich eine spezifische Version)
- Die
webpack.config.jskonfigurieren - Deine
package.jsoneinrichten - Mit node_modules kämpfen (hallo, 400MB)
- Babel für die Transpilierung konfigurieren
- Watching im Development einrichten
- Build für Production konfigurieren
- Deinem Ops-Team erklären, warum dein Docker-Image jetzt 2GB groß ist
Wofür? Um import-Statements in JavaScript zu nutzen. Features, die Browser
nativ seit 2017 unterstützen.
Der Webpack-Kaninchenbau
So sah ein typisches Encore-Setup aus:
// webpack.config.js
const Encore = require("@symfony/webpack-encore");
Encore.setOutputPath("public/build/")
.setPublicPath("/build")
.addEntry("app", "./assets/app.js")
.splitEntryChunks()
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.configureBabel((config) => {
config.plugins.push("@babel/plugin-proposal-class-properties");
})
.configureBabelPresetEnv((config) => {
config.useBuiltIns = "usage";
config.corejs = 3;
})
.enableSassLoader()
.enablePostCssLoader()
.autoProvidejQuery();
module.exports = Encore.getWebpackConfig();
Das ist ein vereinfachtes Beispiel. Ich habe Produktions-Configs mit 300+ Zeilen gesehen.
Und dann brauchte deine package.json dutzende Dependencies:
{
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@symfony/webpack-encore": "^4.0.0",
"core-js": "^3.21.0",
"css-loader": "^6.7.0",
"file-loader": "^6.2.0",
"node-sass": "^7.0.0",
"sass-loader": "^12.0.0",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.0",
"webpack-notifier": "^1.15.0"
}
}
Jedes einzelne dieser Packages brachte seine eigenen Dependencies mit. Die Summe? 374 Packages in node_modules.
AssetMapper: Die radikale Vereinfachung
AssetMapper stellt eine einfache Frage: Was wäre, wenn wir einfach… nicht?
Was wäre, wenn wir nicht transpilieren? Wenn wir nicht bündeln? Wenn wir darauf vertrauen, dass Browser das tun, was sie seit Jahren können?
Hier ist das komplette Setup für ein neues Symfony-Projekt mit AssetMapper:
symfony new mein-projekt --webapp
Das war’s. Nein, wirklich. Symfony 6.3+ bringt AssetMapper standardmäßig mit.
Deine “Konfiguration”? Eine einzige PHP-Datei:
// importmap.php
<?php
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
];
Das ist alles, was du zum Starten brauchst. Du willst eine Third-Party-Library hinzufügen? Ein Befehl:
php bin/console importmap:require bootstrap
Das lädt Bootstraps JavaScript nach assets/vendor/bootstrap/, fügt es zu
deiner Importmap hinzu, und fertig. Kein npm, keine package.json, keine
node_modules.
Wie es tatsächlich funktioniert
AssetMapper nutzt zwei native Browser-Features, die es schon seit Jahren gibt:
1. ES-Module
Moderne Browser verstehen import und export nativ:
// assets/controllers/hello_controller.js
export default class HelloController {
connect() {
console.log("Hallo von Stimulus!");
}
}
Keine Transpilierung nötig. Das funktioniert in jedem Browser, der seit 2017 erschienen ist.
2. Import Maps
Import Maps sagen dem Browser, wo er Module findet:
<script type="importmap">
{
"imports": {
"app": "/assets/app-a3f8b9c2.js",
"bootstrap": "/assets/vendor/bootstrap/bootstrap-5.3.0.js",
"@hotwired/stimulus": "/assets/vendor/@hotwired/stimulus.js"
}
}
</script>
Das bedeutet, du kannst schreiben:
import { Modal } from "bootstrap";
Und der Browser weiß genau, wo er es findet. Kein Bundler erforderlich.
Browser-Support? 94% der User haben native Unterstützung, und Symfony bringt einen Polyfill für die restlichen 6% mit.
Dein erstes AssetMapper-Projekt
Lass uns was Echtes bauen. Wir erstellen eine Symfony-App mit Stimulus-Controllern und Turbo für dieses SPA-artige Feeling.
Schritt 1: Symfony installieren
symfony new asset-demo --webapp
cd asset-demo
AssetMapper ist bereits konfiguriert. Check mal
config/packages/asset_mapper.yaml:
framework:
asset_mapper:
paths:
- assets/
Schritt 2: Dependencies hinzufügen
Fügen wir Stimulus und Turbo hinzu:
php bin/console importmap:require @hotwired/stimulus
php bin/console importmap:require @hotwired/turbo
Diese Befehle:
- Laden die Packages nach
assets/vendor/ - Fügen sie zu
importmap.phphinzu - Locken die Versionen in
importmap.lock
Kein npm install. Kein Warten. Keine 400MB node_modules.
Schritt 3: Einen Stimulus-Controller erstellen
// assets/controllers/counter_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["count"];
connect() {
this.count = 0;
this.updateDisplay();
}
increment() {
this.count++;
this.updateDisplay();
}
updateDisplay() {
this.countTarget.textContent = this.count;
}
}
Schritt 4: Controller registrieren
// assets/app.js
import { Application } from "@hotwired/stimulus";
import CounterController from "./controllers/counter_controller.js";
// Stimulus starten
const application = Application.start();
application.register("counter", CounterController);
Schritt 5: Im Template nutzen
{# templates/home/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div data-controller="counter">
<p>Zähler: <span data-counter-target="count">0</span></p>
<button data-action="click->counter#increment">
Erhöhen
</button>
</div>
{% endblock %}
Schritt 6: Assets im Base-Template einbinden
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Willkommen!{% endblock %}</title>
{% block stylesheets %}
{{ importmap('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Die importmap('app')-Funktion:
- Rendert die Import Map
- Bindet den Polyfill für ältere Browser ein
- Lädt deinen Entry Point (
app.js) - Fügt Versions-Hashes für Cache-Busting hinzu
App starten
symfony serve
Besuche http://localhost:8000 und klick den Button. Es funktioniert. Mit null
Konfiguration, null Build-Zeit und null Node.js.
Real-World-Nutzung: Bootstrap hinzufügen
Fügen wir Bootstrap 5 mit custom Sass hinzu:
php bin/console importmap:require bootstrap
Für das CSS brauchen wir Sass-Support. Installiere das Sass-Bundle:
composer require symfonycasts/sass-bundle
Jetzt erstelle dein custom Stylesheet:
// assets/styles/app.scss
// Bootstrap-Variablen anpassen
$primary: #007bff;
$border-radius: 0.5rem;
// Bootstrap importieren
@import "bootstrap/scss/bootstrap";
// Deine eigenen Styles
.hero {
background: linear-gradient(135deg, $primary, darken($primary, 15%));
color: white;
padding: 3rem;
border-radius: $border-radius;
}
Importiere es in deinem JavaScript:
// assets/app.js
import "./styles/app.scss";
import { Application } from "@hotwired/stimulus";
import { Modal, Dropdown } from "bootstrap";
// Bootstrap-Komponenten global verfügbar machen, falls nötig
window.bootstrap = { Modal, Dropdown };
// Stimulus initialisieren...
Das Sass-Bundle kompiliert dein SCSS on-the-fly im Development und pre-kompiliert für Production.
Migration von Webpack Encore
Du hast ein bestehendes Encore-Projekt? Hier ist der Migrationspfad.
Was du einfach migrieren kannst
- Stimulus-Controller: Funktionieren as-is
- Turbo: Drop-in-Replacement
- Vanilla JavaScript: Funktioniert meist unverändert
- CSS/Sass: Funktioniert mit sass-bundle
- Statische Assets: Bilder, Fonts, etc.
Was einen Build-Step braucht (behalte Encore)
- React-Komponenten: Brauchen JSX-Transpilierung
- Vue Single-File-Components: Brauchen .vue-Kompilierung
- TypeScript: Braucht Transpilierung (obwohl es jetzt ein TypeScript-Bundle gibt)
Migrations-Schritte
1. Deine Assets auditieren
# Was nutzt du eigentlich?
ls assets/
Wenn es hauptsächlich Stimulus-Controller und CSS sind, bist du goldrichtig für die Migration.
2. Neuen Branch erstellen
git checkout -b migrate-to-assetmapper
3. AssetMapper installieren
composer require symfony/asset-mapper symfony/stimulus-bundle
4. Encore entfernen
composer remove symfony/webpack-encore-bundle
rm webpack.config.js package.json package-lock.json
rm -rf node_modules/
5. Deine Assets mappen
# config/packages/asset_mapper.yaml
framework:
asset_mapper:
paths:
- assets/
excluded_patterns:
- "*/tests/*"
- "*.map"
6. Dependencies konvertieren
Für jedes Package in deiner package.json, führe aus:
php bin/console importmap:require package-name
Wichtiger Fallstrick: Wenn du spezifische Dateien aus einem Package importiert hast, musst du diese Dateien requiren:
# Alter Encore-Weg
# import Routing from 'fos-js-routing-bundle';
# AssetMapper-Weg - require die spezifische Datei
php bin/console importmap:require fos-js-routing-bundle/routing.js
7. Templates aktualisieren
{# Alter Encore-Weg #}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }}
{# Neuer AssetMapper-Weg #}
{{ importmap('app') }}
8. Entry Point aktualisieren
// assets/app.js
// Entferne Encore-spezifisches Zeug wie:
// import './bootstrap.js'; // Falls es nur Stimulus gestartet hat
// Behalte deinen eigentlichen App-Code
import { Application } from "@hotwired/stimulus";
import "./styles/app.scss";
const application = Application.start();
// Controller registrieren...
9. Bilder und Fonts behandeln
{# Alter Weg #}
<img src="{{ asset('build/images/logo.png') }}">
{# Neuer Weg #}
<img src="{{ asset('images/logo.png') }}">
AssetMapper versioniert automatisch alle Dateien in deinem
assets/-Verzeichnis.
10. Alles testen
symfony serve
Klick dich durch. Check die Browser-Console. Die meisten Dinge funktionieren einfach.
Häufige Migrations-Probleme
Problem: Module not found
Failed to resolve module specifier "lodash"
Lösung: Du hast vergessen, es zu importmap:require:
php bin/console importmap:require lodash
Problem: Kann spezifische Funktion nicht importieren
// Das funktioniert vielleicht nicht
import { debounce } from "lodash";
Lösung: Importiere die spezifische Datei:
php bin/console importmap:require lodash-es
Dann:
import { debounce } from "lodash-es";
Produktions-Deployment
Wenn du bereit bist zu deployen, kompiliere deine Assets:
php bin/console asset-map:compile
Das:
- Kopiert alle Assets nach
public/assets/mit versionierten Dateinamen - Generiert
manifest.jsonfür schnelle Lookups - Pre-rendert die Import Map für maximale Performance
Dein public/assets/-Verzeichnis wird so aussehen:
public/assets/
├── app-a3f8b9c2.js
├── controllers/
│ └── counter_controller-d8e7f6a1.js
├── vendor/
│ └── @hotwired/
│ └── stimulus-e9f0a8b7.js
├── manifest.json
└── importmap.json
Füge das zu deiner .gitignore hinzu:
/public/assets/
Und zu deinem Deployment-Script:
php bin/console asset-map:compile
Docker-Deployment
Dein Dockerfile wird wunderschön einfach:
FROM php:8.3-fpm
# PHP-Extensions installieren
RUN docker-php-ext-install pdo pdo_mysql
# Anwendung kopieren
WORKDIR /app
COPY . .
# PHP-Dependencies installieren
RUN composer install --no-dev --optimize-autoloader
# Assets kompilieren (kein Node.js nötig!)
RUN php bin/console asset-map:compile
# Das war's
CMD ["php-fpm"]
Kein Node.js-Base-Image. Kein npm install. Kein Webpack-Build. Das Image ist 60% kleiner.
Performance-Überlegungen
Die guten Nachrichten
- Null Build-Zeit im Development: Datei ändern, refreshen, Änderungen sehen
- Kleinere Docker-Images: Kein Node.js, keine node_modules
- Browser-Caching: Jede Datei hat ihren eigenen Hash, cache sie für immer
- Parallele Downloads: Browser kann Module parallel anfordern
- Native Browser-Features: Kein Runtime-Overhead durch Bundler-Code
Der Reality-Check
AssetMapper macht andere Tradeoffs als Webpack:
Mehr HTTP-Requests: Statt einem großen app.js-Bundle hast du vielleicht 20
kleinere Dateien.
Ist das schlecht? Nicht wirklich. Mit HTTP/2 sind parallele Requests günstig. Und du lädst nur das runter, was sich zwischen Deploys geändert hat, nicht komplette Bundles neu.
Kein Tree-Shaking: Du importierst die ganze Library, nicht nur die Teile, die du nutzt.
// Webpack kann das tree-shaken zu nur debounce
import { debounce } from "lodash";
// AssetMapper lädt alles von lodash
import { debounce } from "lodash-es";
Mitigation: Nutze Packages, die bereits modular sind. Bootstrap 5 zum Beispiel lässt dich nur die Komponenten importieren, die du brauchst:
// Lade nur Modal, nicht ganz Bootstrap
import { Modal } from "bootstrap";
Real-World-Performance
Ich habe eine Produktions-App migriert mit:
- 30 Stimulus-Controllern
- Bootstrap 5
- Custom Sass
- 15 Third-Party-Libraries
Vorher (Webpack Encore):
- Initial Load: 487 KB (1 JavaScript-Bundle, 1 CSS-Datei)
- Build-Zeit: 12 Sekunden
- Docker-Image: 1.2 GB
Nachher (AssetMapper):
- Initial Load: 423 KB (28 JavaScript-Dateien, 1 CSS-Datei)
- Build-Zeit: 1.8 Sekunden
- Docker-Image: 512 MB
Die Seite lud tatsächlich schneller, weil:
- Kleinere Gesamt-Bytes (kein Bundler-Overhead)
- Besseres Caching (Änderung eines Controllers invalidiert nicht alles)
- Browser kann kritische Module priorisieren
Die TypeScript-Frage
“Aber ich brauche TypeScript!”, höre ich dich sagen.
Gute Nachrichten: Es gibt ein TypeScript-Bundle für AssetMapper.
composer require sensiolabs/typescript-bundle
Es transpiliert TypeScript-Dateien on-the-fly im Development und pre-kompiliert für Production. Du schreibst:
// assets/controllers/typed_controller.ts
import { Controller } from "@hotwired/stimulus";
interface CounterValue {
count: number;
timestamp: Date;
}
export default class extends Controller<HTMLDivElement> {
static targets = ["count"];
declare readonly countTarget: HTMLElement;
private value: CounterValue;
connect(): void {
this.value = { count: 0, timestamp: new Date() };
this.updateDisplay();
}
increment(): void {
this.value.count++;
this.value.timestamp = new Date();
this.updateDisplay();
}
private updateDisplay(): void {
this.countTarget.textContent = this.value.count.toString();
}
}
Es funktioniert einfach. Type-Checking, Autocompletion, das volle Programm.
Wann du NICHT AssetMapper nutzen solltest
Seien wir ehrlich über die Limitierungen.
Bleib bei Encore/Vite, wenn du nutzt:
React mit JSX
// Das braucht Transpilierung
function App() {
return <div>Hallo Welt</div>;
}
Vue Single-File-Components
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
data() {
return { message: "Hallo Vue!" };
},
};
</script>
Heavy Optimization Requirements: Wenn du eine öffentliche Site mit Millionen Usern baust und jedes letzte KB optimieren musst, könnte Webpacks Tree-Shaking und Code-Splitting die Komplexität wert sein.
Aber bedenke das:
Die meisten Symfony-Apps sind:
- Interne Admin-Panels
- B2B-Anwendungen
- API-Backends mit minimalem Frontend
- Progressiv enhanced Websites
Für diese? AssetMapper ist perfekt. Die Einfachheit zahlt sich jeden einzelnen Tag aus.
Das größere Bild: Weniger ist mehr
AssetMapper repräsentiert einen philosophischen Wandel. Wir haben ein Jahrzehnt damit verbracht, Komplexitätsschichten hinzuzufügen, um Probleme zu lösen, die Browser sowieso lösen würden.
Wir haben ES6 zu ES5 transpiliert… bis Browser ES6 unterstützten. Wir haben Module gebündelt… bis Browser ES-Module unterstützten. Wir haben komplexe Build-Pipelines gebaut… bis wir merkten, dass wir sie nicht brauchen.
AssetMapper ist Symfonys Aussage: “Vertrauen wir der Plattform.”
Es ist dieselbe Philosophie, die zu Folgendem führte:
- Native CSS-Variablen statt Sass-Variablen nutzen
- Native
fetch()statt jQuery.ajax nutzen - Natives
<dialog>statt Modal-Libraries nutzen
Die Plattform ist jetzt gut genug. Nutzen wir sie.
Heute loslegen
Hier ist dein Aktionsplan:
Für neue Projekte:
symfony new mein-projekt --webapp
Du nutzt bereits AssetMapper. Fang einfach an zu bauen.
Für bestehende Projekte:
- Erstelle eine kleine experimentelle App mit AssetMapper
- Mach dich mit dem Workflow vertraut
- Migriere ein risikoarmes bestehendes Projekt
- Evaluiere die Ergebnisse
- Triff eine Entscheidung basierend auf Fakten, nicht auf Angst
Die Alternativen (Und warum ich AssetMapper gewählt habe)
Webpack Encore
✅ Pros:
- Ausgereift, gut dokumentiert
- Volle Kontrolle über Build-Prozess
- Exzellentes Tree-Shaking
- Funktioniert mit React/Vue
❌ Cons:
- Braucht Node.js-Ökosystem
- Komplexe Konfiguration
- Langsame Build-Zeiten
- Großer Dependency-Tree
- Webpack zu debuggen ist eine spezielle Art von Hölle
Verdict: Immer noch die richtige Wahl für React/Vue-Apps, aber overkill für Stimulus-basierte Projekte.
Vite
✅ Pros:
- Blazingly fast im Development
- Bessere DX als Webpack
- Modern und aktiv entwickelt
- Großartig für SPAs
❌ Cons:
- Braucht immer noch Node.js
- Noch ein Tool zum Lernen
- Noch eine Config-Datei
- Overkill für Symfonys typische Use Cases
Verdict: Großartiges Tool, aber löst Probleme, die AssetMapper komplett umgeht.
AssetMapper
✅ Pros:
- Null Konfiguration
- Kein Node.js erforderlich
- Instant Refresh im Development
- PHP-nativer Workflow
- Kleinere Docker-Images
- Perfekt für Stimulus/Turbo
❌ Cons:
- Kein Tree-Shaking
- Kann kein React JSX oder Vue SFCs nutzen
- Mehr HTTP-Requests (durch HTTP/2 mitigiert)
- Neuer, weniger battle-tested
Verdict: Die pragmatische Wahl für moderne Symfony-Anwendungen, die kein schweres Frontend-Framework brauchen.
Fazit: Die Einfachheit hat gewonnen
Drei Tage. So lange habe ich letzten Monat damit verbracht, eine Webpack-Konfiguration zu debuggen, die nach dem Update einer einzigen Dependency kaputt ging. Drei Tage GitHub-Issues lesen, verschiedene Loader-Konfigurationen ausprobieren und langsam den Verstand verlieren.
Mit AssetMapper wäre das äquivalente Problem gewesen: Importmap-Entry updaten. Fertig.
Die Zukunft der Symfony-Frontend-Entwicklung ist nicht mehr Komplexität. Es ist weniger. Es ist darauf zu vertrauen, dass Browser ihren Job machen. Es ist die Plattform zu nutzen, statt gegen sie zu kämpfen.
Ist AssetMapper perfekt? Nein. Wird es für jedes Projekt funktionieren? Auch nein.
Aber für die 80% der Symfony-Projekte, die einfach nur etwas Interaktivität zu server-gerenderten Seiten hinzufügen müssen? Es ist transformativ.
Kein Build-Step. Kein Node.js. Kein Webpack. Kein Problem.
Probier’s aus. Dein zukünftiges Ich wird es dir danken, wenn du nicht um 3 Uhr morgens Webpack-Konfigurationen debuggst.
Ressourcen:
- Offizielle AssetMapper-Dokumentation
- AssetMapper-Component auf GitHub
- Upgrading to AssetMapper Guide
- Import Maps Browser Support
- ES Module Shims Polyfill
- TypeScript-Bundle für AssetMapper
Was ist deine Webpack-Horror-Story? Teil sie in den Kommentaren - geteiltes Leid ist halbes Leid, und ich würde gerne hören, dass ich nicht alleine bin.