Tailwind CSS Purge und JIT: Performance-Auswirkungen verstehen
60fps
ms
Performance · Tailwind CSS · JIT · Build-Pipeline
Tailwind CSS Purge und JIT: Performance-Auswirkungen verstehen
Warum Content-Konfiguration über CSS-Bundle-Größe entscheidet

Tailwinds JIT-Engine generiert ausschließlich jene Utility-Klassen, die tatsächlich im Quellcode vorkommen, doch falsch konfigurierte Content-Pfade sorgen in der Praxis regelmäßig dafür, dass CSS-Bundles unnötig aufgebläht werden oder Styles im Produktivbetrieb plötzlich fehlen, weil der Scanner Templates, Module oder dynamisch zusammengesetzte Klassennamen schlicht übersieht.

13 Min. Lesezeit JIT-Engine · Content-Config · Safelist Tailwind CSS v4 · Hyvä Theme · PostCSS

1. Was Tailwind Purge und JIT wirklich lösen

Vor der JIT-Engine erzeugte Tailwind CSS beim Build zunächst die komplette Utility-Bibliothek für alle Farben, Abstände, Breakpoints und Zustandsvarianten, was in der Entwicklung zu CSS-Dateien im zweistelligen Megabyte-Bereich führte. Ein nachgelagerter Schritt namens Purge, meist über PurgeCSS, scannte anschließend den kompilierten Output und entfernte alle Selektoren, die im Quellcode nicht vorkamen. Dieses Modell war fehleranfällig: PurgeCSS arbeitete auf der fertigen Stylesheet-Ebene und musste raten, welche Klassen tatsächlich verwendet wurden, was regelmäßig zu False Positives und False Negatives führte. Mit Tailwind 2.1 wurde die JIT-Engine eingeführt, seit Version 3 ist sie der einzige Kompilierungsmodus. Statt zu generieren und danach zu entfernen, generiert JIT von Anfang an nur die Klassen, die tatsächlich referenziert werden.

Für Magento- und Hyvä-Projekte ist dieser Unterschied keine akademische Feinheit, sondern hat direkte Auswirkungen auf jede einzelne Seitenauslieferung. Ein falsch konfigurierter Content-Pfad zieht im schlimmsten Fall Templates aus nicht relevanten Modulen oder sogar aus node_modules in den Scan, was das ausgelieferte CSS-Bundle unnötig vergrößert, den Parser länger blockiert und die Time-to-Interactive verzögert. Dieser Artikel behandelt die Engineering-Mechanik dahinter: wie die JIT-Engine Klassen erkennt, wie Content-Konfiguration und die neue @source-Direktive in Tailwind v4 korrekt eingesetzt werden, und wie sich die tatsächliche CSS-Größe vorher und nachher konkret messen lässt.

2. Die JIT-Engine: On-Demand-Generierung von Utility-Klassen

Die JIT-Engine arbeitet in zwei Phasen. In der ersten Phase durchsucht ein regelbasierter Scanner jede Datei, die von der Content-Konfiguration erfasst wird, nach Zeichenketten, die wie Utility-Klassen aussehen. Wichtig dabei: Der Scanner parst kein HTML, kein PHP und kein JavaScript im eigentlichen Sinn, sondern behandelt jede Datei als reinen Text und extrahiert Tokens über einen Regex, der grob dem Muster gültiger Klassennamen entspricht, inklusive Doppelpunkt-Notation für Varianten wie hover: oder lg: und eckigen Klammern für beliebige Werte wie [mask-type:luminance]. Dieser bewusst naive Ansatz ist der Grund, warum Tailwind mit praktisch jeder Templatesprache funktioniert, von phtml über Twig bis JSX, ohne einen dedizierten Parser pro Framework zu benötigen.

In der zweiten Phase versucht die Engine, jeden gefundenen Kandidaten gegen die interne Utility-Grammatik aufzulösen. Passt ein Token, wird die entsprechende CSS-Regel just-in-time erzeugt und dem Output hinzugefügt, andernfalls wird der Kandidat verworfen. Diese Architektur ist zusätzlich inkrementell: Im Watch-Modus, wie er bei bin/start für den Hyvä-Tailwind-Watcher aktiv ist, hält die Engine den vorherigen Kandidaten-Cache im Speicher und verarbeitet bei einer Dateiänderung nur das Delta. Dadurch liegen Rebuilds im Entwicklungsbetrieb typischerweise im Bereich weniger Millisekunden, während der alte PurgeCSS-Ansatz bei jeder Änderung das komplette generierte Stylesheet neu analysieren musste.

3. Content-Konfiguration: Globs korrekt für akkurates Purging setzen

Die Content-Konfiguration definiert, welche Dateien die JIT-Engine überhaupt scannt, und ist damit die wichtigste Stellschraube für korrektes Verhalten. In Tailwind v3 ist das ein Array von Glob-Mustern in tailwind.config.js, zum Beispiel content: ['./app/design/frontend/**/*.phtml']. Jede Datei, die einem dieser Muster entspricht, wird bei jedem Build gelesen und gescannt, das heißt: Umfang und Anzahl der erfassten Dateien wirken sich direkt auf die Build-Zeit aus. Ein zu breites Glob wie ./**/*.phtml im Projekt-Root erfasst versehentlich auch vendor/-Verzeichnisse mit Templates aus Drittanbieter-Modulen, die im eigenen Theme gar nicht gerendert werden, was Build-Zeit kostet und im Zweifel zusätzliche, ungenutzte Utility-Klassen generiert.

Das gegenteilige Problem ist subtiler und schwerer zu debuggen: ein zu enges Glob, das legitime Template-Verzeichnisse ausschließt. In einer Magento/Hyvä-Struktur gehören dazu typischerweise das Parent-Theme unter vendor/hyva-themes/magento2-default-theme-csp, eigene Module unter app/code/*/*/view/frontend/templates und gegebenenfalls Layout-XML-Dateien, wenn dort Klassennamen als Argumente übergeben werden. Fehlt einer dieser Pfade im Glob, generiert die JIT-Engine für dort verwendete Klassen schlicht keine Regel, die Klasse steht im HTML, aber es existiert keine passende CSS-Regel dafür. Das Ergebnis ist ein unstyliertes Element ohne jede Fehlermeldung, ein Bugtyp, der besonders in Codereviews leicht übersehen wird, weil der Code selbst syntaktisch korrekt aussieht.

4. Tailwind v4 CSS-First: @source-Direktive statt content-Array

Tailwind v4 ersetzt die JavaScript-Konfigurationsdatei durch einen CSS-first-Ansatz. Statt tailwind.config.js mit einem content-Array steht die gesamte Konfiguration direkt im Stylesheet, eingeleitet mit @import "tailwindcss";. Für die Content-Erkennung bringt v4 eine automatische Heuristik mit, die Projektverzeichnisse anhand von .gitignore-Regeln durchsucht und dabei typische Ausschlussverzeichnisse wie node_modules automatisch überspringt. In einer gewachsenen Magento-Struktur mit symlinked Vendor-Modulen und mehreren Theme-Ebenen reicht diese Automatik jedoch selten aus, weshalb weiterhin explizite @source-Direktiven nötig sind.

Eine wichtige, oft übersehene Änderung beim Umstieg von v3 auf v4: Pfade in @source werden relativ zur CSS-Datei selbst aufgelöst, nicht relativ zum Projekt-Root wie das alte content-Array. Ein Glob, das unter v3 korrekt funktionierte, kann nach der Migration ins Leere laufen, weil sich der Bezugspunkt verschoben hat, ohne dass der Build mit einem Fehler reagiert, er generiert einfach zu wenig CSS. Mit @source not "pfad"; lassen sich zusätzlich Verzeichnisse gezielt aus einem sonst automatisch erfassten Scope ausschließen, etwa Testfixtures oder Storybook-Demos, die zwar Tailwind-Klassen enthalten, aber niemals im Produktivbetrieb ausgeliefert werden.


/* web/tailwind/tailwind-source.css - Tailwind v4 CSS-first configuration */
@import "tailwindcss";

/* Explicit source paths, resolved relative to THIS file, not the project root */
@source "../../../../app/design/frontend/Mironsoft/default/**/*.phtml";
@source "../../../../vendor/hyva-themes/magento2-default-theme-csp/**/*.phtml";
@source "../../../../app/code/*/*/view/frontend/templates/**/*.phtml";
@source "../../../../app/code/*/*/view/frontend/layout/**/*.xml";

/* Exclude paths that would otherwise match via auto-detection */
@source not "../../../../app/code/*/*/Test/**/*";
@source not "../../../../vendor/hyva-themes/*/src/**/demo/**/*";

@theme {
  --color-brand-500: #dc2626;
}

5. Häufige Fehlkonfiguration: fehlende Styles vs. aufgeblähtes CSS

In der Praxis zeigt sich Fehlkonfiguration in zwei entgegengesetzten Symptomen. Das erste ist die fehlende Style-Regel durch dynamisch zusammengesetzte Klassennamen, etwa class="text-<?= $color ?>-600" in einem phtml-Template oder ein Alpine-Ausdruck wie :class="`bg-${status}-100`". Die JIT-Engine sieht in beiden Fällen niemals den vollständigen, literalen Token text-red-600, sondern nur Fragmente, aus denen sich keine gültige Utility ableiten lässt. Das Resultat ist besonders tückisch, weil kein Fehler geworfen wird: Das Element trägt die Klasse im DOM, DevTools zeigt sie im Elemente-Panel an, aber unter „Computed" fehlt jede passende Regel, weil sie nie generiert wurde.

Das zweite Symptom ist aufgeblähtes CSS durch zu breite Content-Globs, die Beispielverzeichnisse, Dokumentation oder ungenutzte Legacy-Templates mit einscannen. Zur Diagnose hilft ein einfacher Zweischritt: Fehlt eine erwartete Regel im generierten Output, prüft man zunächst, ob der vollständige Klassenname als Literal im Quellcode auftaucht, nicht als zusammengesetzter String. Ist der Output unerwartet groß, hilft ein gezieltes Deaktivieren einzelner Content-Globs mit anschließendem Rebuild und Größenvergleich, um den Verursacher einzugrenzen, statt die gesamte Konfiguration zu raten.


/* Generated output excerpt - BEFORE: content glob accidentally included
   vendor/*/Magento_Backend admin templates in the scan */
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
.bg-admin-panel-950 { background-color: #030712; } /* admin-only, never rendered in storefront */
.text-\[13\.5px\] { font-size: 13.5px; } /* arbitrary value from a backend-only grid template */
/* ...several hundred more admin-only utilities generated unnecessarily... */

/* Generated output excerpt - AFTER: content glob scoped to storefront templates only */
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
/* admin-only utilities are no longer generated, output shrinks accordingly */

/* Dynamic class construction: JIT never sees the complete literal token */
/* class="text-<?= $color ?>-600" in phtml produces no rule at all,
   because "text-" and "-600" are separate string fragments at scan time */

6. Safelist: dynamisch erzeugte Klassennamen gezielt erhalten

Manche dynamischen Klassennamen lassen sich architektonisch nicht vermeiden, etwa wenn Redakteure im Magento-Admin eine Badge-Farbe aus einer Auswahlliste konfigurieren und diese als Datenbankwert in einen Klassennamen einfließt. Für genau diesen Fall existiert in Tailwind v3 die safelist-Option in tailwind.config.js, die sowohl exakte Klassennamen als auch Regex-Muster akzeptiert und diese unabhängig vom JIT-Scan immer generiert. Ein Beispiel: safelist: [{ pattern: /bg-(red|green|blue)-(100|500|700)/ }] erzwingt die Generierung aller neun Kombinationen, selbst wenn kein einziger vollständiger Token im Quellcode auftaucht.

In Tailwind v4 übernimmt @source inline("...") diese Rolle direkt im CSS, inklusive Unterstützung für Brace-Expansion, was intern zum kartesischen Produkt aller Kombinationen expandiert wird. Safelisting ist bewusst als Notlösung zu verstehen, nicht als primärer Mechanismus: Wer die Muster zu großzügig fasst, etwa die komplette Farbskala statt der tatsächlich im Admin verfügbaren Werte, hebelt den eigentlichen Vorteil der JIT-Engine wieder aus und produziert erneut ungenutzte Regeln. Die Regel lautet daher, Safelist-Patterns so eng wie möglich an den tatsächlichen Wertebereich der dynamischen Quelle zu binden.


/* Tailwind v4: force generation of dynamically composed admin badge colors
   that the JIT scanner can never find as literal tokens in the source */
@source inline("bg-{red,green,blue,amber}-{100,500,700}");
@source inline("text-{red,green,blue,amber}-{700,800}");

/* Narrow the pattern to the actual admin value domain instead of the full scale */
/* WRONG - too broad, regenerates dozens of unused shades */
@source inline("bg-{red,orange,amber,yellow,lime,green,emerald,teal,cyan,blue}-{50,100,200,300,400,500,600,700,800,900}");

/* RIGHT - limited to the four badge colors and two shades actually used in the admin grid */
@source inline("bg-{red,green,blue,amber}-{100,700}");

7. CSS-Dateigröße messen: vorher/nachher mit konkreten Zahlen

Die Rohgröße einer generierten CSS-Datei ist für sich genommen wenig aussagekräftig, weil Utility-CSS durch die massive Wiederholung kurzer Selektoren und Deklarationen extrem gut komprimiert. Aussagekräftig ist die tatsächliche Transfergröße nach Gzip- oder Brotli-Kompression, da genau die über das Netzwerk beim Kunden ankommt. Ein schneller Check ohne Server-Roundtrip: gzip -9 -c dist/css/styles.css | wc -c liefert die komprimierte Byte-Zahl direkt im Terminal. Für ein gesund konfiguriertes Hyvä-Storefront-Theme liegt dieser Wert typischerweise zwischen 40 und 70 Kilobyte gzip-komprimiert, unabhängig von der unkomprimierten Rohgröße, die durchaus 400 bis 700 Kilobyte betragen kann.

Ein einfacher, aber wirkungsvoller Workflow: Vor einer Änderung an der Content-Konfiguration Rohgröße und Gzip-Größe notieren, danach neu bauen und erneut messen. Springt die Gzip-Größe um mehr als 20 bis 30 Prozent nach oben, deutet das fast immer auf ein zu breites Content-Glob hin, nicht auf tatsächlich neu benötigte Utilities. Für dauerhafte Absicherung empfiehlt sich ein CI-Schritt mit einem Größenbudget, etwa über das size-limit-Package, der den Build fehlschlagen lässt, sobald das generierte CSS einen definierten Schwellenwert überschreitet, statt die Regression erst später in einem Lighthouse-Audit oder durch Nutzer-Feedback zu entdecken.


# Build production CSS bundle and inspect raw + gzip size
bin/npm --prefix app/design/frontend/Mironsoft/default/web/tailwind run build

du -h dist/css/styles.css
# 612K  dist/css/styles.css

gzip -9 -c dist/css/styles.css | wc -c
# 58382   (~57 KB gzip-compressed, healthy range)

# After a content glob regression accidentally pulling in vendor/ admin templates
du -h dist/css/styles.css
# 4.2M  dist/css/styles.css

gzip -9 -c dist/css/styles.css | wc -c
# 312044  (~305 KB gzip-compressed, regression confirmed)

8. Hyvä's Tailwind-Build-Pipeline: Besonderheiten und Stolperfallen

Hyvä bringt eine eigene tailwind.config.js pro Theme mit, die typischerweise require('tailwindcss/defaultTheme') importiert und die Content-Globs so aufbaut, dass sowohl das eigene Theme als auch das Parent-Theme unter vendor/hyva-themes/magento2-default-theme-csp erfasst werden. Bei eigenen Modulen mit Frontend-Templates in app/code/*/*/view/frontend/templates muss dieser Pfad explizit ergänzt werden, sonst bleiben dort verwendete Utility-Klassen ungeneriert. Ein häufiger Stolperstein: Nach dem Anlegen eines neuen Modul-Template-Verzeichnisses erscheinen die zugehörigen Styles erst nach einem expliziten Tailwind-Rebuild, weil die Content-Erkennung zur Build-Zeit läuft, nicht bei setup:static-content:deploy.

Die korrekte Deploy-Reihenfolge ist deshalb entscheidend: Erst bin/npm --prefix app/design/frontend/[Vendor]/[Theme]/web/tailwind run build, danach erst var/view_preprocessed und pub/static/frontend leeren und neu deployen. Wird dieser Schritt übersprungen oder in falscher Reihenfolge ausgeführt, kann eine veraltete, bereits durch JIT reduzierte CSS-Datei in Produktion landen, während neue Templates längst neue Klassen referenzieren. Der Hyvä-Watcher, der während bin/start im Hintergrund läuft, deckt diesen Fall in der lokalen Entwicklung zuverlässig ab, ersetzt aber nicht den expliziten Produktions-Build vor dem Deploy.


{
  "size-limit": [
    {
      "name": "Storefront CSS bundle (gzip)",
      "path": "pub/static/frontend/Mironsoft/default/de_DE/css/styles.css",
      "limit": "80 KB",
      "gzip": true
    }
  ],
  "scripts": {
    "build:tailwind": "tailwindcss -i ./web/tailwind/tailwind-source.css -o ./web/css/styles.css --minify",
    "size-check": "size-limit"
  }
}

9. Purge/JIT-Konfiguration im direkten Vergleich

Die folgende Übersicht fasst die häufigsten Konfigurationsszenarien zusammen und zeigt, welchen konkreten Effekt Fehlkonfiguration gegenüber korrekter Konfiguration auf CSS-Größe und Build-Verhalten hat.

Szenario Fehlkonfiguration Empfohlene Konfiguration Effekt
Content-Glob-Scope content: ['./**/*.phtml'] content: ['./app/design/frontend/**/*.phtml'] ~1,8 MB → ~52 KB (gzip)
Dynamische Klassen class="text-${color}-600" class="text-red-600" Style fehlt vs. Style vorhanden
v4 @source-Pfad @source "../../**/*"; @source "../../../../app/code/**/*.phtml"; Rebuild 4,1 s → 210 ms
Safelist-Nutzung Keine Safelist, DB-Farbwerte fehlen Engmaschiges Safelist-Pattern Admin-Badge unstyliert vs. korrekt
Kompressionscheck Nur Rohgröße geprüft gzip -9 vor jedem Release Transfergröße realistisch vs. unterschätzt

In der Praxis treten Content-Glob-Fehler und dynamische Klassennamen häufig gemeinsam auf, weil beide aus demselben Grundproblem entstehen: die JIT-Engine kennt ausschließlich das, was sie als vollständigen Text in einer erfassten Datei findet. Wer beide Ursachen systematisch prüft, findet die meisten CSS-Bloat- und Missing-Style-Bugs innerhalb weniger Minuten, statt tagelang im Dunkeln zu debuggen.

Mironsoft

Tailwind-Build-Pipeline, JIT-Konfiguration und CSS-Performance für Magento- und Hyvä-Shops

Aufgeblähtes CSS-Bundle im Griff behalten?

Wir analysieren eure Content-Konfiguration, identifizieren fehlende und überflüssige Utility-Klassen und reduzieren das ausgelieferte CSS-Bundle messbar, ohne dass dabei Styles im Produktivbetrieb verloren gehen.

Build-Pipeline-Audit

Tailwind-Konfiguration, Content-Globs und JIT-Setup vollständig durchleuchten

Content-Konfiguration optimieren

Fehlende und überschüssige Globs korrigieren, Safelist-Patterns gezielt eingrenzen

CSS-Bundle-Größenreduktion

Gzip-Größe messen, CI-Budget einrichten und Regressionen dauerhaft verhindern

10. Zusammenfassung

Tailwind CSS Purge und JIT lösen dasselbe Grundproblem aus zwei Richtungen: Statt eine komplette Utility-Bibliothek zu generieren und nachträglich zu bereinigen, erzeugt die JIT-Engine seit Tailwind 3 ausschließlich Klassen, die als vollständiger, literaler Token im Quellcode auftauchen. Die Content-Konfiguration, ob als klassisches content-Array oder als @source-Direktive in Tailwind v4, entscheidet dabei über zwei entgegengesetzte Fehlerquellen: zu breite Globs blähen das CSS-Bundle unnötig auf, zu enge Globs führen zu unstylierten Elementen ohne jede Fehlermeldung.

Dynamisch zusammengesetzte Klassennamen sind die häufigste Ursache für fehlende Styles, weil die JIT-Engine nur vollständige Literale erkennt, keine String-Interpolation. Safelist beziehungsweise @source inline() lösen diesen Sonderfall gezielt, sollten aber eng an den tatsächlichen Wertebereich gebunden bleiben. Wer regelmäßig die Gzip-Größe des generierten CSS misst und über ein CI-Budget absichert, erkennt Regressionen, bevor sie in Produktion landen, statt sie erst in einem späteren Performance-Audit zu entdecken.

Tailwind CSS Purge und JIT - Das Wichtigste auf einen Blick

JIT statt Purge

Seit Tailwind v3 generiert die JIT-Engine ausschließlich referenzierte Utility-Klassen, statt eine volle Bibliothek zu erzeugen und nachträglich zu bereinigen.

Content-Globs korrekt setzen

Weder zu breit (Vendor-Verzeichnisse, Build-Zeit) noch zu eng (fehlende Module, fehlende Styles). Jeder Template-Pfad muss explizit erfasst sein.

Dynamische Klassen vermeiden

Vollständige Klassennamen als Literal im Quellcode halten. safelist bzw. @source inline() nur gezielt für unvermeidbare Fälle einsetzen.

Messen statt Raten

Gzip-Größe vor und nach jeder Config-Änderung vergleichen, CI-Größenbudget mit size-limit gegen Regressionen absichern.

11. FAQ: Tailwind CSS Purge und JIT

1Was ist der Unterschied zwischen Purge und JIT?
Purge generierte alles und entfernte danach Ungenutztes. JIT generiert von Anfang an nur die Klassen, die als vollständiger Token im Quellcode gefunden werden.
2Warum heißt es jetzt content statt purge?
Die Option steuert seit JIT nicht mehr, was entfernt wird, sondern was gescannt und generiert wird. Der Name spiegelt diese Rollenänderung wider.
3Wie erkennt JIT, welche Klassen generiert werden?
Ein Regex-Scanner extrahiert Kandidaten-Tokens aus jeder erfassten Datei als reinen Text und prüft sie gegen die Utility-Grammatik.
4Warum fehlen Styles bei dynamischen Klassennamen?
JIT sucht vollständige literale Tokens. Bei zusammengesetzten Strings wie text-${color}-600 existiert der volle Klassenname nie im Quellcode.
5Was ändert sich bei Content-Globs in v4?
@source ersetzt das content-Array und löst Pfade relativ zur CSS-Datei auf, nicht mehr relativ zum Projekt-Root. Ein häufiger Migrationsfehler.
6Wann safelist bzw. @source inline() nutzen?
Nur bei nachweislich dynamischen Klassennamen aus einer Datenquelle, mit Pattern so eng wie möglich am tatsächlichen Wertebereich.
7Wie messe ich die tatsächliche CSS-Größe?
Mit gzip -9 -c styles.css | wc -c die komprimierte Transfergröße prüfen, nicht die Rohgröße, die durch Wiederholung stark irreführt.
8Welche Fehler führen zu aufgeblähtem CSS in Hyvä?
Zu breite Globs, die vendor/-Admin-Templates erfassen, sowie zu großzügige Safelist-Regex-Muster mit viel mehr Kombinationen als benötigt.
9Warum bleibt CSS trotz korrigierter Config groß?
Oft wegen ungenutzter Safelist-Einträge oder vieler individueller arbitrary-value-Klassen. Ein Blick in den Output zeigt die tatsächlichen Verursacher.
10Muss ich nach jedem neuen Template neu bauen?
Im Watch-Modus automatisch. Für Produktion ist ein expliziter Build vor dem Deploy zwingend, static-content:deploy generiert das CSS nicht neu.