From c66c80adcc68f6b454735e9ce140d1ff7ad4c7d4 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sun, 8 Feb 2026 11:22:36 +0100 Subject: [PATCH] perf: lighthouse optimizations - inline critical CSS, lazy-load routes, WebP images, fix CLS and contrast --- .gitignore | 2 + .htaccess | 42 +++++++ AGENTS.md | 7 ++ README.md | 16 ++- build.py | 82 +++++++++++++ css/animate.custom.css | 44 +++++++ css/components.css | 4 +- css/fonts.css | 8 +- css/variables.css | 8 +- deploy.sh | 30 ++--- index.html | 147 ++++++++++++++++++++++-- js/app.js | 5 +- js/components/app-footer.js | 13 ++- js/components/app-shell.js | 68 +++++------ js/components/listing-card.js | 4 +- js/components/pages/page-favorites.js | 2 +- js/components/pages/page-home.js | 22 +++- js/components/pages/page-listing.js | 2 +- js/components/pages/page-my-listings.js | 2 +- js/components/search-box.js | 1 + js/router.js | 31 +++-- js/services/directus.js | 2 +- service-worker.js | 7 +- 23 files changed, 448 insertions(+), 101 deletions(-) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 build.py create mode 100644 css/animate.custom.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ead01d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +__pycache__/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..8d59cb1 --- /dev/null +++ b/.htaccess @@ -0,0 +1,42 @@ + + ExpiresActive On + + # Fonts (1 year - they never change) + ExpiresByType font/woff2 "access plus 1 year" + ExpiresByType application/font-woff2 "access plus 1 year" + + # Images (1 year) + ExpiresByType image/svg+xml "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/webp "access plus 1 year" + ExpiresByType image/x-icon "access plus 1 year" + + # CSS & JS (1 week - updated via deploy) + ExpiresByType text/css "access plus 1 week" + ExpiresByType application/javascript "access plus 1 week" + ExpiresByType text/javascript "access plus 1 week" + + # JSON (1 day - locales may change) + ExpiresByType application/json "access plus 1 day" + + # HTML (no cache - always fresh) + ExpiresByType text/html "access plus 0 seconds" + + # Manifest & Service Worker (no cache) + + ExpiresDefault "access plus 0 seconds" + + + + + # Service Worker must not be cached + + Header set Cache-Control "no-cache, no-store, must-revalidate" + + + # Fonts: immutable + + Header set Cache-Control "public, max-age=31536000, immutable" + + diff --git a/AGENTS.md b/AGENTS.md index d77376b..29352ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,13 @@ python3 -m http.server 8080 # Oder mit Live-Reload npx live-server +# Build (JS/CSS minifizieren → dist/) +pip3 install rjsmin rcssmin # einmalig +python3 build.py + +# Deploy (baut automatisch, dann rsync von dist/) +./deploy.sh + # Tests ausführen (im Browser) # Server starten, dann http://localhost:8080/tests/ öffnen diff --git a/README.md b/README.md index 2a0e974..599dddb 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,18 @@ Für Produktion werden nur diese Dateien benötigt: **Nicht deployen:** `tests/`, `docs/`, `AGENTS.md`, `README.md`, `.git/`, `deploy.sh` +#### Build (Minifizierung) + +```bash +# Einmalig: Dependencies installieren +pip3 install rjsmin rcssmin + +# Build ausführen (erstellt dist/ mit minifizierten Dateien) +python3 build.py +``` + +Das Build-Script minifiziert alle JS- und CSS-Dateien (~111 KiB Ersparnis) und kopiert alles nach `dist/`. + #### Deploy via Script ```bash @@ -126,10 +138,10 @@ Für Produktion werden nur diese Dateien benötigt: ./deploy.sh ``` -Das Script nutzt `rsync` über SSH und synchronisiert nur geänderte Dateien. -Ausgeschlossene Dateien/Ordner (tests, docs, .git, etc.) werden automatisch übersprungen. +Das Script führt automatisch `python3 build.py` aus, dann `rsync` von `dist/` zum Server. **Voraussetzungen:** +- Python 3 + `rjsmin` + `rcssmin` (für Build) - SSH-Key-Authentifizierung zum Server - `rsync` lokal und auf dem Server installiert diff --git a/build.py b/build.py new file mode 100644 index 0000000..802e62d --- /dev/null +++ b/build.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Build script for dgray.io +Minifies JS and CSS files into dist/ directory. + +Usage: python3 build.py +Requirements: pip3 install rjsmin rcssmin +""" + +import os +import shutil +import sys + +try: + import rjsmin + import rcssmin +except ImportError: + print("Missing dependencies. Install with:") + print(" pip3 install rjsmin rcssmin") + sys.exit(1) + +SRC_DIR = os.path.dirname(os.path.abspath(__file__)) +DIST_DIR = os.path.join(SRC_DIR, 'dist') + +EXCLUDE_DIRS = {'.git', 'dist', 'node_modules', 'docs', 'tests', '.vscode', '.idea', '__pycache__'} +EXCLUDE_FILES = {'AGENTS.md', 'deploy.sh', 'build.py', 'README.md', '.gitignore'} + +def should_exclude(path): + parts = os.path.relpath(path, SRC_DIR).split(os.sep) + return any(p in EXCLUDE_DIRS for p in parts) or os.path.basename(path) in EXCLUDE_FILES + +def build(): + if os.path.exists(DIST_DIR): + shutil.rmtree(DIST_DIR) + + js_saved = 0 + css_saved = 0 + files_copied = 0 + + for root, dirs, files in os.walk(SRC_DIR): + dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS] + + for filename in files: + src_path = os.path.join(root, filename) + if should_exclude(src_path): + continue + + rel_path = os.path.relpath(src_path, SRC_DIR) + dest_path = os.path.join(DIST_DIR, rel_path) + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + + if filename.endswith('.js') and not filename.endswith('.min.js'): + with open(src_path, 'r', encoding='utf-8') as f: + original = f.read() + minified = rjsmin.jsmin(original) + with open(dest_path, 'w', encoding='utf-8') as f: + f.write(minified) + saved = len(original.encode('utf-8')) - len(minified.encode('utf-8')) + js_saved += saved + files_copied += 1 + + elif filename.endswith('.css') and not filename.endswith('.min.css'): + with open(src_path, 'r', encoding='utf-8') as f: + original = f.read() + minified = rcssmin.cssmin(original) + with open(dest_path, 'w', encoding='utf-8') as f: + f.write(minified) + saved = len(original.encode('utf-8')) - len(minified.encode('utf-8')) + css_saved += saved + files_copied += 1 + + else: + shutil.copy2(src_path, dest_path) + files_copied += 1 + + print(f"Build complete: {files_copied} files -> dist/") + print(f" JS savings: {js_saved / 1024:.1f} KiB") + print(f" CSS savings: {css_saved / 1024:.1f} KiB") + print(f" Total saved: {(js_saved + css_saved) / 1024:.1f} KiB") + +if __name__ == '__main__': + build() diff --git a/css/animate.custom.css b/css/animate.custom.css new file mode 100644 index 0000000..7fc8786 --- /dev/null +++ b/css/animate.custom.css @@ -0,0 +1,44 @@ +:root { + --animate-duration: 1s; +} + +.animate__animated { + animation-duration: var(--animate-duration); + animation-fill-mode: both; +} + +.animate__faster { + animation-duration: calc(var(--animate-duration) / 2); +} + +@keyframes fadeIn { + 0% { opacity: 0; } + to { opacity: 1; } +} + +.animate__fadeIn { + animation-name: fadeIn; +} + +@keyframes fadeOut { + 0% { opacity: 1; } + to { opacity: 0; } +} + +.animate__fadeOut { + animation-name: fadeOut; +} + +@keyframes heartBeat { + 0% { transform: scale(1); } + 14% { transform: scale(1.3); } + 28% { transform: scale(1); } + 42% { transform: scale(1.3); } + 70% { transform: scale(1); } +} + +.animate__heartBeat { + animation-name: heartBeat; + animation-duration: calc(var(--animate-duration) * 1.3); + animation-timing-function: ease-in-out; +} diff --git a/css/components.css b/css/components.css index 2ed20fe..d1385cd 100644 --- a/css/components.css +++ b/css/components.css @@ -215,7 +215,7 @@ app-header .logo { app-header .logo-img { height: 28px; - width: auto; + width: 100px; } app-header .logo-dark { @@ -299,6 +299,8 @@ app-footer { border-top: 1px solid var(--color-border); padding: var(--space-lg) 0; margin-top: auto; + min-height: 5.5rem; + contain: layout style; } app-footer .footer-inner { diff --git a/css/fonts.css b/css/fonts.css index 63c9385..1e70001 100644 --- a/css/fonts.css +++ b/css/fonts.css @@ -13,7 +13,7 @@ font-family: 'Inter'; font-style: normal; font-weight: 400; - font-display: swap; + font-display: optional; src: url('../assets/fonts/Inter-Regular.woff2') format('woff2'); } @@ -21,7 +21,7 @@ font-family: 'Inter'; font-style: normal; font-weight: 500; - font-display: swap; + font-display: optional; src: url('../assets/fonts/Inter-Medium.woff2') format('woff2'); } @@ -29,7 +29,7 @@ font-family: 'Inter'; font-style: normal; font-weight: 600; - font-display: swap; + font-display: optional; src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2'); } @@ -37,7 +37,7 @@ font-family: 'Inter'; font-style: normal; font-weight: 700; - font-display: swap; + font-display: optional; src: url('../assets/fonts/Inter-Bold.woff2') format('woff2'); } diff --git a/css/variables.css b/css/variables.css index 346250f..c90f360 100644 --- a/css/variables.css +++ b/css/variables.css @@ -26,7 +26,7 @@ --color-text: #1A1A1A; --color-text-secondary: #4A4A4A; - --color-text-muted: #8A8A8A; + --color-text-muted: #737373; --color-border: #D0D0D0; --color-border-focus: #555555; @@ -118,7 +118,7 @@ --color-text: #F0F0F0; --color-text-secondary: #C0C0C0; - --color-text-muted: #808080; + --color-text-muted: #949494; --color-border: #3A3A3A; --color-border-focus: #AAAAAA; @@ -151,7 +151,7 @@ --color-text: #F0F0F0; --color-text-secondary: #C0C0C0; - --color-text-muted: #808080; + --color-text-muted: #949494; --color-border: #3A3A3A; --color-border-focus: #AAAAAA; @@ -182,7 +182,7 @@ --color-text: #1A1A1A; --color-text-secondary: #4A4A4A; - --color-text-muted: #8A8A8A; + --color-text-muted: #737373; --color-border: #D0D0D0; --color-border-focus: #555555; diff --git a/deploy.sh b/deploy.sh index 855570f..2ef8fb6 100755 --- a/deploy.sh +++ b/deploy.sh @@ -14,35 +14,21 @@ set -e REMOTE_USER_HOST="${1:-admin@dgray.io}" REMOTE_PATH="${2:-/home/admin/web/dgray.io/public_html}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DIST_DIR="$SCRIPT_DIR/dist" -# Files/directories to exclude from deployment -EXCLUDES=( - ".git" - ".gitignore" - "AGENTS.md" - "deploy.sh" - "docs/" - "tests/" - "README.md" - ".vscode" - ".idea" -) +# --- Build (minify JS + CSS) --- +echo "Building..." +python3 "$SCRIPT_DIR/build.py" +echo "" -# --- Build exclude arguments --- -EXCLUDE_ARGS="" -for item in "${EXCLUDES[@]}"; do - EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$item" -done - -# --- Deploy --- +# --- Deploy from dist/ --- echo "Deploying dgray.io" -echo " From: $SCRIPT_DIR" +echo " From: $DIST_DIR" echo " To: $REMOTE_USER_HOST:$REMOTE_PATH" echo "" rsync -avz --delete \ - $EXCLUDE_ARGS \ - "$SCRIPT_DIR/" \ + "$DIST_DIR/" \ "$REMOTE_USER_HOST:$REMOTE_PATH/" echo "" diff --git a/index.html b/index.html index 9c8c273..9318846 100644 --- a/index.html +++ b/index.html @@ -33,19 +33,152 @@ - + + + + + + + + + + + + - - - - - + + + + + + + -
+
+ + +
+ + + +
+
+