Bundle-Size-Diffs und Lighthouse-Vergleiche als Pre-Merge-Gate
Performance-Regressionen entstehen selten durch eine einzelne dramatische Änderung, sondern durch viele kleine Verschlechterungen, die einzeln unter dem Radar bleiben. Eine CI-Pipeline, die Bundle-Größe und Lighthouse-Metriken bei jedem Pull Request automatisch gegen den Basis-Branch vergleicht, macht diese schleichende Verschlechterung sichtbar, bevor sie den Hauptzweig und damit die Produktion erreicht.
Inhaltsverzeichnis
- 1. Pre-Merge-Gate vs. Post-Merge-Monitoring
- 2. Aufbau einer Regressionstest-Pipeline
- 3. Bundle-Size-Tracking pro Pull Request
- 4. Diffs automatisch im Pull Request kommentieren
- 5. Lighthouse-Score-Diffing zwischen Branches
- 6. Statistisch signifikante Änderungen automatisch erkennen
- 7. Rauschen in CI-Runnern beherrschen
- 8. CI-Laufzeitkosten gegen Gründlichkeit abwägen
- 9. Build-Artefakte cachen und Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Pre-Merge-Gate vs. Post-Merge-Monitoring
Ein Pre-Merge-Gate prüft Performance-Metriken, bevor Code in den Hauptzweig gelangt, und blockiert oder markiert den Pull Request bei einer Regression. Post-Merge-Monitoring über Real User Monitoring oder synthetische Checks auf Produktion erkennt Regressionen erst, nachdem sie bereits live sind. Beide Ansätze schließen sich nicht aus, sie lösen unterschiedliche Probleme: Post-Merge-Monitoring deckt Effekte auf, die nur unter echter Last, echten Netzwerken und echten Geräten sichtbar werden. Ein Pre-Merge-Gate verhindert, dass offensichtliche Regressionen überhaupt erst dorthin gelangen.
Der wirtschaftliche Unterschied liegt in den Korrekturkosten. Eine Regression, die im Pull Request auffällt, kostet den Autor wenige Minuten Nacharbeit, während derselbe Fehler in Produktion oft erst nach Tagen über sinkende Conversion-Zahlen oder eine CrUX-Verschlechterung auffällt und dann durch mehrere Commits, einen Revert oder ein Hotfix behoben werden muss. Für Magento- und Hyvä-Shops mit häufigen Deployments ist ein Pre-Merge-Gate deshalb keine Ergänzung, sondern die erste Verteidigungslinie, während Post-Merge-Monitoring die Kontrolle über die tatsächliche Nutzererfahrung behält.
2. Aufbau einer Regressionstest-Pipeline
Eine funktionierende Regressionstest-Pipeline braucht drei Bausteine: einen reproduzierbaren Build für den Pull-Request-Branch, eine Baseline-Messung für den Basis-Branch (meist main) und einen Vergleichsschritt, der beide Messungen gegenüberstellt. Die Baseline wird idealerweise nicht bei jedem PR neu gemessen, sondern nach jedem Merge in main einmal erzeugt und als Artefakt oder in einem kleinen Datenspeicher abgelegt. Das spart CI-Zeit und stellt sicher, dass alle Pull Requests gegen denselben, stabilen Referenzwert verglichen werden statt gegen einen erneut gemessenen, potenziell verrauschten Wert.
Der Vergleichsschritt selbst sollte als eigenständiger Job laufen, der Build und Baseline als Eingabe nimmt und einen strukturierten Diff als Ausgabe erzeugt, zum Beispiel im JSON-Format. Diese Trennung erlaubt es, denselben Vergleichslogik-Code sowohl für Bundle-Size- als auch für Lighthouse-Diffs wiederzuverwenden und ihn unabhängig vom eigentlichen Build-Prozess zu testen. Wichtig ist außerdem, dass die Pipeline denselben Build-Prozess für PR-Branch und Baseline verwendet, denn unterschiedliche Node- oder PHP-Versionen, unterschiedliche NODE_ENV-Werte oder abweichende Tailwind-Konfigurationen verfälschen den Vergleich unabhängig von echten Code-Änderungen.
3. Bundle-Size-Tracking pro Pull Request
Bundle-Size-Tracking misst die Größe der ausgelieferten JavaScript- und CSS-Dateien nach dem Build und vergleicht sie gegen denselben Build auf dem Basis-Branch. Entscheidend ist, nicht die unkomprimierte Rohgröße zu vergleichen, sondern die Größe nach Gzip oder Brotli, weil das der tatsächlich über das Netzwerk übertragenen Datenmenge entspricht. Für Hyvä-Themes bedeutet das konkret: web/tailwind/tailwind.css nach dem Purge-Schritt und die generierten Alpine.js-Bundles unter pub/static/frontend/ sind die relevanten Artefakte, nicht der unminifizierte Quellcode.
Ein einfaches, robustes Bash-Pattern checkt beide Branches nacheinander aus, baut sie identisch und summiert die Dateigrößen pro Kategorie. Der Diff wird als Prozentsatz relativ zur Basisgröße berechnet, nicht als absolute Byte-Differenz, weil ein Delta von 5 KB bei einem 20-KB-Bundle etwas völlig anderes bedeutet als bei einem 500-KB-Bundle. Ein Schwellenwert von beispielsweise 5 % Wachstum pro Kategorie lässt normale Entwicklung zu, verhindert aber, dass sich unbemerkte Aufblähung über viele kleine Pull Requests hinweg ansammelt.
#!/usr/bin/env bash
# bundle-size-diff.sh - Compare gzip bundle size between two git refs
set -euo pipefail
BASE_REF="${1:-origin/main}"
HEAD_REF="${2:-HEAD}"
THRESHOLD_PERCENT=5
BUNDLE_GLOB="pub/static/frontend/**/*.{js,css}"
measure_bundle_size() {
local ref="$1"
local worktree
worktree="$(mktemp -d)"
git worktree add --detach "$worktree" "$ref" >/dev/null
( cd "$worktree" && npm ci --silent && npm run build --silent )
# Sum gzip-compressed size of all matching assets in bytes
find "$worktree/pub/static/frontend" -type f \( -name "*.js" -o -name "*.css" \) \
-exec gzip -c {} \; | wc -c
git worktree remove --force "$worktree"
}
base_size="$(measure_bundle_size "$BASE_REF")"
head_size="$(measure_bundle_size "$HEAD_REF")"
delta=$(( head_size - base_size ))
percent=$(awk -v b="$base_size" -v d="$delta" 'BEGIN { printf "%.2f", (d / b) * 100 }')
echo "Base: ${base_size} bytes | Head: ${head_size} bytes | Delta: ${delta} bytes (${percent}%)"
if awk -v p="$percent" -v t="$THRESHOLD_PERCENT" 'BEGIN { exit !(p > t) }'; then
echo "[FAIL] Bundle grew by ${percent}%, exceeds threshold of ${THRESHOLD_PERCENT}%"
exit 1
fi
echo "[OK] Bundle size within threshold"
4. Diffs automatisch im Pull Request kommentieren
Ein Zahlenwert im CI-Log wird von den wenigsten Entwicklern aktiv gesucht. Deutlich wirksamer ist ein automatischer PR-Kommentar, der Bundle-Size- und Lighthouse-Diff direkt sichtbar macht, dort wo der Code-Review ohnehin stattfindet. Die gängige Technik dafür ist ein Sticky Comment: Statt bei jedem Pipeline-Lauf einen neuen Kommentar zu erzeugen, sucht der Bot nach einem vorhandenen Kommentar mit einem eindeutigen Marker (zum Beispiel einem versteckten HTML-Kommentar <!-- perf-bot-marker -->) und aktualisiert diesen, statt den Thread mit wiederholten Kommentaren zuzumüllen.
Der Kommentar sollte nicht nur die Gesamtzahl zeigen, sondern pro Datei oder Chunk aufschlüsseln, wo die Größenänderung herkommt, inklusive eines Links zum vollständigen Lighthouse-Report als Artefakt. Für Teams mit mehreren Storefronts oder Marken empfiehlt sich eine Tabelle pro Seite (Startseite, Kategorieseite, Produktseite), weil eine Regression oft nur eine einzelne Seitenvorlage betrifft und eine aggregierte Gesamtzahl das verschleiern würde. Ein status: failure auf dem entsprechenden GitHub-Check zusätzlich zum Kommentar macht das Ergebnis auch für Branch-Protection-Regeln verwertbar.
# .github/workflows/performance-regression.yml
name: Performance Regression Check
on:
pull_request:
branches: [main]
jobs:
bundle-and-lighthouse-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
# Restore cached base-branch build to avoid rebuilding main every run
- name: Restore baseline artifact
uses: actions/cache/restore@v4
with:
path: .baseline
key: baseline-${{ github.event.pull_request.base.sha }}
- name: Build PR branch
run: npm ci && npm run build
- name: Compare bundle size
id: bundle
run: bash ./ci/bundle-size-diff.sh origin/${{ github.base_ref }} HEAD >> "$GITHUB_OUTPUT"
- name: Run Lighthouse CI on both branches
run: npx @lhci/cli autorun --config=./lighthouserc.json
- name: Post or update sticky PR comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: perf-bot-marker
message: |
## Performance Regression Report
${{ steps.bundle.outputs.summary }}
5. Lighthouse-Score-Diffing zwischen Branches
Lighthouse-Score-Diffing geht einen Schritt über reines Bundle-Tracking hinaus, weil eine kleinere JavaScript-Datei nicht automatisch eine schnellere Seite bedeutet. Die Pipeline baut beide Branches, deployed sie auf identisch konfigurierte, temporäre Umgebungen (zum Beispiel über Docker-Container mit demselben Seed-Datensatz) und führt Lighthouse CI gegen dieselben URLs auf beiden Umgebungen aus. Verglichen werden nicht nur der Performance-Score, sondern die Einzelmetriken LCP, TBT (Total Blocking Time als Labor-Näherung für INP) und CLS, weil eine Verschlechterung oft nur eine einzelne Metrik betrifft, während der aggregierte Score sie durch Verbesserungen in anderen Metriken verdeckt.
Entscheidend für belastbare Ergebnisse ist eine identische Testumgebung für beide Läufe: gleiche CPU-Drosselung, gleiche Netzwerk-Simulation, gleiche Anzahl an Wiederholungen. Lighthouse CI unterstützt das über numberOfRuns in der Konfiguration, wodurch mehrere Läufe pro URL erfolgen und der Median statt eines Einzelwerts verwendet wird. Für Magento-Shops mit Produktseiten, die je nach Kategorie unterschiedlich viele Bilder und Varianten laden, sollten mehrere repräsentative Seitentypen getestet werden, nicht nur die Startseite, da diese oft die schnellste und am wenigsten aussagekräftige Seite im gesamten Shop ist.
{
"ci": {
"collect": {
"url": [
"http://localhost:3000/",
"http://localhost:3000/catalog/damen-jacken.html",
"http://localhost:3000/catalog/product/view/id/1284"
],
"numberOfRuns": 5,
"settings": {
"throttlingMethod": "simulate",
"throttling": {
"cpuSlowdownMultiplier": 4,
"rttMs": 150,
"throughputKbps": 1600
}
}
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.85 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"total-blocking-time": ["error", { "maxNumericValue": 300 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
6. Statistisch signifikante Änderungen automatisch erkennen
Ein einfacher Zahlenvergleich zwischen zwei Lighthouse-Läufen führt zu unbrauchbaren Ergebnissen, weil jeder einzelne Lauf bereits ohne Code-Änderung um mehrere hundert Millisekunden schwankt. Die Lösung ist, nicht einen einzelnen Wert, sondern eine Verteilung aus mehreren Läufen pro Branch zu vergleichen und erst dann eine Regression zu melden, wenn der Unterschied zwischen den Medianen eine definierte Toleranzschwelle überschreitet, üblicherweise kombiniert aus einem relativen Prozentsatz (zum Beispiel 10 % LCP-Verschlechterung) und einem absoluten Mindestwert (zum Beispiel mindestens 150 Millisekunden), damit winzige Prozentsprünge bei bereits sehr schnellen Metriken nicht fälschlich als Regression zählen.
Ein Node-Skript, das zwei Lighthouse-JSON-Reports einliest, eignet sich gut, um diese Logik zentral zu implementieren und für alle Metriken konsistent anzuwenden, statt sie in jedem Workflow neu zu duplizieren. Wichtig ist, dass das Skript pro Metrik unterschiedliche Schwellenwerte erlaubt, weil CLS naturgemäß in einem viel kleineren Wertebereich schwankt als LCP und deshalb einen anderen relativen Schwellenwert braucht. Das Ergebnis sollte als strukturiertes JSON zurückgegeben werden, damit es sowohl für den PR-Kommentar als auch für den Exit-Code der Pipeline weiterverwendet werden kann.
// compare-lighthouse-reports.js - Flag statistically significant regressions
const fs = require('node:fs');
const METRIC_THRESHOLDS = {
'largest-contentful-paint': { relativePercent: 10, absoluteMinMs: 150 },
'total-blocking-time': { relativePercent: 15, absoluteMinMs: 50 },
'cumulative-layout-shift': { relativePercent: 15, absoluteMinMs: 0.01 },
};
function median(values) {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}
function loadMedianMetrics(reportPaths) {
const runs = reportPaths.map((p) => JSON.parse(fs.readFileSync(p, 'utf8')));
const result = {};
for (const metric of Object.keys(METRIC_THRESHOLDS)) {
result[metric] = median(runs.map((r) => r.audits[metric].numericValue));
}
return result;
}
function diffMetrics(baseReports, headReports) {
const base = loadMedianMetrics(baseReports);
const head = loadMedianMetrics(headReports);
const regressions = [];
for (const [metric, { relativePercent, absoluteMinMs }] of Object.entries(METRIC_THRESHOLDS)) {
const delta = head[metric] - base[metric];
const percent = (delta / base[metric]) * 100;
// Flag only if BOTH the relative and absolute thresholds are exceeded
if (percent > relativePercent && delta > absoluteMinMs) {
regressions.push({ metric, base: base[metric], head: head[metric], percent: percent.toFixed(1) });
}
}
return regressions;
}
const regressions = diffMetrics(process.argv.slice(2, 6), process.argv.slice(6, 10));
console.log(JSON.stringify({ regressions, hasRegression: regressions.length > 0 }, null, 2));
process.exit(regressions.length > 0 ? 1 : 0);
7. Rauschen in CI-Runnern beherrschen
CI-Runner auf geteilten Cloud-Infrastrukturen liefern keine konstante Rechenleistung: Nachbar-Container auf demselben Host, wechselnde CPU-Generationen und variable Netzwerklatenz erzeugen eine Streuung, die bei einer Einzelmessung leicht mit einer echten Regression verwechselt wird. Wer diese Varianz ignoriert, bekommt entweder ständig False Positives, die das Team dazu bringen, Warnungen zu ignorieren, oder senkt die Schwellenwerte so weit, dass echte Regressionen ebenfalls durchrutschen. Beides untergräbt das Vertrauen in die gesamte Pipeline.
Die wirksamste Gegenmaßnahme ist die Kombination aus mehreren Wiederholungen pro Messung (fünf Lighthouse-Läufe statt einem), der Verwendung des Medians statt des Mittelwerts, weil der Median robuster gegen einzelne Ausreißer ist, und dedizierten, gleich dimensionierten Runnern für performance-kritische Jobs statt der günstigsten verfügbaren Instanz. Zusätzlich hilft ein Warm-up-Lauf vor der eigentlichen Messung, der nicht gewertet wird, aber Caching-Effekte auf Betriebssystem- und Docker-Ebene ausgleicht, sodass der erste gemessene Lauf nicht systematisch langsamer ist als die folgenden.
8. CI-Laufzeitkosten gegen Gründlichkeit abwägen
Ein vollständiger Lighthouse-Audit mit fünf Wiederholungen über mehrere Seitentypen und beide Branches kostet schnell mehrere Minuten CI-Zeit pro Pull Request, was sich bei einem aktiven Team mit zwanzig PRs am Tag zu erheblichen Kosten und langen Wartezeiten summiert. Nicht jeder Pull Request braucht denselben Prüfumfang: Eine reine Textänderung im CMS-Content oder eine Übersetzungsdatei hat keinen Einfluss auf Bundle-Größe oder Rendering-Performance und muss den vollen Audit nicht durchlaufen.
Ein praktikabler Ansatz ist Path-basiertes Filtering: Der volle Regressionstest läuft nur, wenn sich Dateien in web/`, `Magento_Theme/templates oder JavaScript-/CSS-Quellverzeichnissen geändert haben. Für alle anderen PRs reicht ein schneller, günstiger Bundle-Size-Check ohne vollen Lighthouse-Lauf. Ergänzend eignet sich ein Label-gesteuerter Full-Audit, den Reviewer manuell mit einem perf-audit-Label auslösen, sowie ein nächtlicher, vollständiger Lauf gegen main, der Drift über Zeit erkennt, die einzelne PR-Diffs wegen kumulativ kleiner Änderungen unter dem Schwellenwert übersehen.
9. Build-Artefakte cachen und Strategien im Vergleich
Caching von Build-Artefakten ist der zweite große Hebel neben Sampling, um die Pipeline schnell zu halten. Der Node-Modules-Cache, der Tailwind-JIT-Cache und vor allem der bereits gebaute Baseline-Branch sollten zwischen Pipeline-Läufen wiederverwendet werden, statt bei jedem PR neu erzeugt zu werden. Ein Cache-Key, der den Lockfile-Hash und den Basis-Branch-Commit-SHA kombiniert, stellt sicher, dass der Cache exakt dann invalidiert wird, wenn sich Abhängigkeiten oder die Baseline tatsächlich geändert haben, und nicht öfter.
Die folgende Übersicht vergleicht gängige Ansätze zur Regressionserkennung in der CI-Pipeline nach Erkennungszeitpunkt und typischer Schwachstelle.
| Strategie | Erkennungszeitpunkt | Typische Schwachstelle | Empfehlung |
|---|---|---|---|
| Kein automatisierter Check | Erst nach dem Deploy | Regression erreicht Produktion unbemerkt | Automatisiertes Pre-Merge-Gate einführen |
| Manueller PR-Review | Vor dem Merge, aber unzuverlässig | Reviewer übersieht Bundle-Wachstum von wenigen KB | Automatischer Bot-Kommentar mit Diff |
| Nur Post-Merge-RUM | Tage bis Wochen nach dem Merge | Nutzer bereits betroffen, Ursache schwer rückverfolgbar | Pre-Merge-Gate als Ergänzung, nicht Ersatz |
| Rohvergleich einzelner Lighthouse-Läufe | Vor dem Merge, aber verrauscht | Flaky Fails durch CI-Runner-Varianz | Median aus mehreren Läufen plus Toleranzband |
| Kombiniertes Bundle- und Lighthouse-Gate mit Sampling | Vor dem Merge, gezielt gewichtet | Erfordert initialen Konfigurationsaufwand | Path-Filter plus nächtlicher Full-Audit gegen main |
Für GitLab CI sieht die Cache-Konfiguration technisch anders aus als bei GitHub Actions, folgt aber demselben Prinzip: Abhängigkeiten und Build-Ausgaben über einen stabilen Key zwischenspeichern, damit wiederholte Pipeline-Läufe nicht bei null anfangen.
# .gitlab-ci.yml - Cache dependencies and build output to keep the pipeline fast
performance-regression:
stage: test
image: node:20-alpine
rules:
- changes:
- "src/app/design/frontend/**/web/**/*"
- "src/app/design/frontend/**/*.phtml"
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
variables:
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm"
before_script:
- npm ci --prefer-offline --cache .npm
script:
- npm run build
- bash ./ci/bundle-size-diff.sh origin/main HEAD
- npx @lhci/cli autorun --config=./lighthouserc.json
artifacts:
when: always
paths:
- .lighthouseci/
expire_in: 14 days
Mironsoft
Performance-Engineering, CI/CD-Pipelines und Monitoring für Magento-Shops
Performance-Regressionen automatisch abfangen?
Wir richten Bundle-Size-Tracking und Lighthouse-Diffing als Pre-Merge-Gate in eurer CI-Pipeline ein, inklusive robuster Schwellenwerte gegen CI-Rauschen und einer Sampling-Strategie, die eure Pipeline schnell hält.
CI-Pipeline-Audit
Bestehende Pipeline analysieren und Regressionslücken identifizieren
Gate-Implementierung
Bundle-Diff, Lighthouse CI und PR-Kommentare produktiv einrichten
Kosten-Optimierung
Caching und Sampling, damit die Pipeline schnell und günstig bleibt
10. Zusammenfassung
Performance-Regressionstests in der CI-Pipeline lösen ein Problem, das rein manuelle Code-Reviews strukturell nicht lösen können: die Summe vieler kleiner, für sich genommen unauffälliger Verschlechterungen. Ein Pre-Merge-Gate, das Bundle-Größe nach Gzip und Lighthouse-Kernmetriken automatisch gegen den Basis-Branch vergleicht, macht diese Verschlechterung sichtbar, bevor sie in Produktion gelangt. Entscheidend für die Akzeptanz im Team ist, dass die Pipeline nicht durch CI-Rauschen ständig falschen Alarm schlägt: Median aus mehreren Läufen, kombinierte relative und absolute Schwellenwerte sowie ein Sticky-PR-Kommentar statt eines vergrabenen Log-Eintrags sorgen dafür, dass Entwickler den Warnungen tatsächlich vertrauen und darauf reagieren.
Gründlichkeit und CI-Laufzeit stehen dabei in einem direkten Zielkonflikt, der sich nicht durch mehr Rechenleistung allein lösen lässt. Path-basiertes Filtering, damit reine Content-Änderungen keinen vollen Audit auslösen, gecachte Baseline-Builds statt wiederholter Neuberechnung und ein nächtlicher Full-Audit als Sicherheitsnetz gegen kumulative Drift halten die Pipeline für die meisten Pull Requests unter einer Minute, ohne bei tatsächlich relevanten Änderungen an Aussagekraft zu verlieren.
Performance-Regressionstests in der CI-Pipeline - Das Wichtigste auf einen Blick
Pre-Merge-Gate
Bundle-Size- und Lighthouse-Vergleich vor dem Merge fängt Regressionen ab, bevor sie Produktion erreichen. Ergänzt, ersetzt nicht Post-Merge-Monitoring.
Bundle-Size-Diff
Gzip-Größe pro Kategorie gegen Basis-Branch vergleichen, Ergebnis als prozentualer Schwellenwert, nicht als absolute Byte-Differenz.
Statistische Signifikanz
Median aus mehreren numberOfRuns, kombinierte relative und absolute Schwellenwerte gegen CI-Rauschen und False Positives.
Kosten & Caching
Path-basiertes Filtering, gecachte Baseline-Builds und nächtlicher Full-Audit halten die Pipeline schnell und günstig.