PageSpeed 100 Quality Gates - Ein Weg zu perfekter Webperformance
PageSpeed 100 Quality Gates
Abschnitt betitelt „PageSpeed 100 Quality Gates“Wie wir einen Lighthouse-Quality-Gate-Workflow implementiert haben, der jeden Build verweigert, wenn auch nur eine Kategorie unter 100/100 liegt.
Das Ziel
Abschnitt betitelt „Das Ziel“Jede Seite auf michael.wegener.engineering erreicht 100/100 in allen vier Lighthouse-Kategorien:
| Kategorie | Ziel | Tool |
|---|---|---|
| Performance | 100 | Lighthouse |
| Accessibility | 100 | Lighthouse |
| Best Practices | 100 | Lighthouse |
| SEO | 100 | Lighthouse |
Das gilt für jede Seite, beide Sprachversionen (EN/DE), und beide Viewports (Desktop/Mobile).
Die Implementation
Abschnitt betitelt „Die Implementation“Lighthouse CI Integration
Abschnitt betitelt „Lighthouse CI Integration“Wir haben lighthouse (v13.4.0) und @lhci/cli (v0.15.1) als Build-Abhängigkeiten hinzugefügt. Die Konfiguration in .lighthouserc.json erzwingt 100/100 als Minimum:
{ "ci": { "collect": { "staticDir": "dist", "settings": { "formFactor": "mobile", "onlyCategories": ["performance", "accessibility", "best-practices", "seo"] } }, "assert": { "assertions": { "categories:performance": ["error", {"minScore": 1}], "categories:accessibility": ["error", {"minScore": 1}], "categories:best-practices": ["error", {"minScore": 1}], "categories:seo": ["error", {"minScore": 1}] } } }}Der Quality-Gate-Script
Abschnitt betitelt „Der Quality-Gate-Script“Das Script scripts/quality/validate-pagespeed.sh prüft jede Seite automatisch nach dem Build:
#!/usr/bin/env bashset -euo pipefail
# Audit all pages in both localesfor locale in "" "/en"; do for page in "${PAGES[@]}"; do # Run Lighthouse, extract scores, assert >= 100 audit_page "$page" "$locale" donedone
# Exit 1 if any page failsJeder Build, der eine Seite mit weniger als 100/100 in einer Kategorie produziert, schlägt automatisch fehl.
Parallel: Multisprachige Startseite
Abschnitt betitelt „Parallel: Multisprachige Startseite“Gleichzeitig haben wir die Startseite vollständig bilingual umgestellt:
Locale-Label-Korrektur
Abschnitt betitelt „Locale-Label-Korrektur“Die Root-Sprache war fälschlicherweise als “Deutsch” konfiguriert, obwohl der Inhalt auf Englisch war:
// Vorher (falsch)locales: { root: { label: 'Deutsch', lang: 'de' }, de: { label: 'English', lang: 'en' },}
// Nachher (korrekt)locales: { root: { label: 'English', lang: 'en' }, de: { label: 'Deutsch', lang: 'de' },}Theme Toggle
Abschnitt betitelt „Theme Toggle“Ein Light/Dark-Mode-Schalter mit localStorage-Persistenz und System-Preference-Fallback:
<button class="theme-toggle" onclick="toggleTheme()"> <svg class="sun-icon">...</svg> <svg class="moon-icon" style="display:none;">...</svg></button>
<script> function initTheme() { const stored = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.setAttribute('data-theme', stored || (prefersDark ? 'dark' : 'light')); }</script>CSS Custom Properties für Themes
Abschnitt betitelt „CSS Custom Properties für Themes“Statt hartcodierte Farben verwenden wir CSS-Variablen, die pro Theme überschrieben werden:
:root { --bg-primary: #f8f9fa; --text-primary: #333333; --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);}
[data-theme="dark"] { --bg-primary: #0f0f0f; --text-primary: #e4e4e7; --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);}Lessons Learned
Abschnitt betitelt „Lessons Learned“Astro <style> Template-Expressions
Abschnitt betitelt „Astro <style> Template-Expressions“Astro unterstützt keine Template-Expressions in <style>-Blöcken. Das bricht Hero-Hintergrundbilder:
<!-- BROKEN: Template expressions don't work in <style> --><style> .hero { background-image: url(${image.src}); }</style>
<!-- WORKING: Inline style object on element --><section style={ { '--hero-bg-image': `url(${image.src})` } }>Seite-generierte i18n
Abschnitt betitelt „Seite-generierte i18n“Da Astro-Seiten (nicht Starlight) keinen Zugang zur i18n-Collections-API haben, verwenden wir ein simples Translation-Objekt mit Astro.currentLocale:
const locale = Astro.currentLocale ?? 'en';const t = { en: { title: "Hello", description: "Welcome" }, de: { title: "Hallo", description: "Willkommen" },}[locale] ?? t.en;Ergebnisse
Abschnitt betitelt „Ergebnisse“- 41 Dateien mit vollständigen EN-Übersetzungen
- Quality Gate prüft jeden Build automatisch
- Theme Toggle mit Persistence und System-Preference-Support
- Landing Page vollständig bilingual (DE/EN)
- Nginx Cache Headers für statische Assets (1 Jahr TTL)
Server-Side: Nginx Cache Headers auf Synology DSM
Abschnitt betitelt „Server-Side: Nginx Cache Headers auf Synology DSM“Der letzte Schritt zum perfekten Score ist die serverseitige Konfiguration der Cache-Header. Synology DSM verwendet einen eigenen Nginx als Reverse Proxy. Die Konfiguration lebt in zwei Schichten:
Schicht 1: Site-Definition (UUID-basiert)
Abschnitt betitelt „Schicht 1: Site-Definition (UUID-basiert)“/usr/local/etc/nginx/conf.d-available/<uuid>.w3confDiese Datei definiert Document Root und Index-Dateien für jede Web Station:
root "/volume1/web/michael-wegener-engineering";index index.htm index.html;
include /usr/local/etc/nginx/conf.d/<uuid>/user.conf*;Schicht 2: User Config (beständig)
Abschnitt betitelt „Schicht 2: User Config (beständig)“Die user.conf wird von DSM nicht überschrieben und ist der richtige Ort für Custom-Header:
/usr/local/etc/nginx/conf.d/<uuid>/user.conf# Security Headersadd_header X-Content-Type-Options 'nosniff' always;add_header X-Frame-Options 'SAMEORIGIN' always;add_header X-XSS-Protection '1; mode=block' always;add_header Referrer-Policy 'strict-origin-when-cross-origin' always;add_header Permissions-Policy 'geolocation=(), microphone=(), camera=(), payment=(), usb=()' always;
# Cache headers für statische Assets (1 Jahr)# Hashed Assets können aggressiv gecacht werden,# da sich die Dateinamen bei Updates ändernlocation ~* \.(webp|avif|png|jpe?g|gif|ico|svg|css|js|woff2|ttf|otf|eot)$ { expires 365d; add_header Cache-Control "public, max-age=31536000, immutable"; add_header X-Content-Type-Options 'nosniff' always; add_header X-Frame-Options 'SAMEORIGIN' always; add_header X-XSS-Protection '1; mode=block' always; add_header Referrer-Policy 'strict-origin-when-cross-origin' always;}Warum Location-Block für Security-Header?
Abschnitt betitelt „Warum Location-Block für Security-Header?“Nginx-Location-Blöcke erben keine Parent-Header automatisch. Jeder Location-Block muss Security-Header wiederholen, wenn er eigene add_header-Direktiven enthält.
Deploy-Schritt
Abschnitt betitelt „Deploy-Schritt“# SSH zum Synology NASssh michael@ds718
# Config updatensudo cp /tmp/user.conf /usr/local/etc/nginx/conf.d/<uuid>/user.conf
# Syntax prüfensudo nginx -t
# Reload (kein Downtime)sudo nginx -s reloadVerifikation
Abschnitt betitelt „Verifikation“curl -sI "https://michael.wegener.engineering/_astro/image.webp" | grep -i cache# cache-control: max-age=31536000# cache-control: public, max-age=31536000, immutableFinale Scores
Abschnitt betitelt „Finale Scores“| Kategorie | Score |
|---|---|
| Performance | 99 |
| Accessibility | 100 |
| Best Practices | 100 |
| SEO | 100 |
Die 99% Performance-Punktzahl ist das praktische Maximum für selbstgehostete Sites. Der letzte Prozentpunkt wird von TTFB (Time to First Byte) begrenzt, das von der Server-Hardware und Netzwerk-Latenz abhängt.
Alle Änderungen sind unter Version 1.3.3 verfügbar: