Performance: GZip, Cache-Control, WebP, SQLite-Tuning, Indizes, srcset — SW by-v438, APP_VER 417
This commit is contained in:
parent
5bd07d9598
commit
e0c2b2bdc1
10 changed files with 46 additions and 12 deletions
|
|
@ -26,6 +26,9 @@ def get_connection() -> sqlite3.Connection:
|
|||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
conn.execute("PRAGMA busy_timeout=5000")
|
||||
conn.execute("PRAGMA cache_size = -32000") # 32MB Page-Cache
|
||||
conn.execute("PRAGMA temp_store = MEMORY") # Temp-Tabellen im RAM statt auf Disk
|
||||
conn.execute("PRAGMA mmap_size = 268435456") # 256MB Memory-Mapped I/O
|
||||
conn.create_function('norm', 1, _norm)
|
||||
return conn
|
||||
|
||||
|
|
@ -1212,3 +1215,11 @@ def _migrate(conn_factory):
|
|||
)
|
||||
""")
|
||||
logger.info("Migration: dogs.gewicht_kg aus health synchronisiert.")
|
||||
|
||||
# Performance-Indizes für häufige Queries
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)")
|
||||
logger.info("Migration: Performance-Indizes bereit.")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from fastapi import FastAPI, Request
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from database import init_db
|
||||
|
|
@ -98,6 +99,20 @@ class _CacheControlMiddleware(BaseHTTPMiddleware):
|
|||
app.add_middleware(_CacheControlMiddleware)
|
||||
|
||||
|
||||
class MediaCacheMiddleware(BaseHTTPMiddleware):
|
||||
"""Setzt aggressive Cache-Header für /media/-Requests.
|
||||
UUID-basierte Dateinamen ändern sich nie → immutable caching.
|
||||
"""
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
if request.url.path.startswith('/media/'):
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
|
||||
return response
|
||||
|
||||
app.add_middleware(MediaCacheMiddleware)
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# API-Router registrieren (werden nach und nach hinzugefügt)
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ _PREVIEW_MAX = 800 # längste Seite in Pixeln
|
|||
|
||||
|
||||
def generate_preview(data: bytes, ext: str) -> bytes | None:
|
||||
"""Erzeugt ein JPEG-Preview (max _PREVIEW_MAX px). Gibt None zurück bei Videos/PDFs/Fehler."""
|
||||
"""Erzeugt ein WebP-Preview (max _PREVIEW_MAX px). Gibt None zurück bei Videos/PDFs/Fehler."""
|
||||
if ext.lower() not in _PREVIEW_EXTS:
|
||||
return None
|
||||
try:
|
||||
|
|
@ -172,7 +172,7 @@ def generate_preview(data: bytes, ext: str) -> bytes | None:
|
|||
img = img.convert("RGB")
|
||||
img.thumbnail((_PREVIEW_MAX, _PREVIEW_MAX), Image.LANCZOS)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=72, optimize=True)
|
||||
img.save(buf, format="WEBP", quality=80)
|
||||
return buf.getvalue()
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -185,11 +185,11 @@ def preview_url_from(url: str | None) -> str | None:
|
|||
return None
|
||||
low = url.lower()
|
||||
if any(low.endswith(s) for s in (
|
||||
".mp4", ".webm", ".mov", ".avi", ".pdf", "_thumb.jpg", "_preview.jpg"
|
||||
".mp4", ".webm", ".mov", ".avi", ".pdf", "_thumb.jpg", "_preview.jpg", "_preview.webp"
|
||||
)):
|
||||
return None
|
||||
base, _ = os.path.splitext(url)
|
||||
return base + "_preview.jpg"
|
||||
return base + "_preview.webp"
|
||||
|
||||
|
||||
def extract_video_thumb(video_path: str) -> str | None:
|
||||
|
|
|
|||
|
|
@ -829,7 +829,7 @@ async def generate_media_previews(user=Depends(require_admin)):
|
|||
base, ext = os.path.splitext(fname)
|
||||
if ext.lower() not in _PREVIEW_EXTS:
|
||||
continue
|
||||
preview_path = os.path.join(folder, base + "_preview.jpg")
|
||||
preview_path = os.path.join(folder, base + "_preview.webp")
|
||||
if os.path.exists(preview_path):
|
||||
skipped += 1
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -686,7 +686,7 @@ async def upload_media(dog_id: int, entry_id: int,
|
|||
elif media_type == "image":
|
||||
preview_bytes = generate_preview(raw_data, ext)
|
||||
if preview_bytes:
|
||||
preview_path = os.path.splitext(path)[0] + "_preview.jpg"
|
||||
preview_path = os.path.splitext(path)[0] + "_preview.webp"
|
||||
with open(preview_path, "wb") as f:
|
||||
f.write(preview_bytes)
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def _save_upload(file: UploadFile, data: bytes) -> str:
|
|||
else:
|
||||
preview_bytes = generate_preview(data, ext)
|
||||
if preview_bytes:
|
||||
with open(os.path.splitext(path)[0] + "_preview.jpg", "wb") as f:
|
||||
with open(os.path.splitext(path)[0] + "_preview.webp", "wb") as f:
|
||||
f.write(preview_bytes)
|
||||
return f"/media/forum/{filename}"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '416'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '417'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
|
|||
|
|
@ -598,7 +598,9 @@ window.Page_diary = (() => {
|
|||
content.innerHTML = `<div class="diary-media-mosaic">${
|
||||
allMedia.map(m => `
|
||||
<div class="diary-mosaic-item" data-entry-id="${m.entryId}" data-full-url="${UI.escape(m.url)}">
|
||||
<img src="${UI.escape(m.preview_url || m.url)}" alt="" loading="lazy"
|
||||
<img src="${UI.escape(m.preview_url || m.url)}"
|
||||
${m.preview_url ? `srcset="${UI.escape(m.preview_url)} 800w, ${UI.escape(m.url)} 2000w" sizes="(max-width:400px) 200px, 400px"` : ''}
|
||||
alt="" loading="lazy"
|
||||
onerror="this.src='${UI.escape(m.url)}'">
|
||||
</div>`).join('')
|
||||
}</div>`;
|
||||
|
|
@ -839,7 +841,10 @@ window.Page_diary = (() => {
|
|||
</div>`;
|
||||
} else {
|
||||
photoHtml = `<div class="diary-card-photo">
|
||||
<img src="${e.cover_preview_url || e.cover_url || coverMedia.preview_url || coverMedia.url}" alt="Foto" loading="lazy">
|
||||
<img src="${e.cover_preview_url || e.cover_url || coverMedia.preview_url || coverMedia.url}"
|
||||
${(e.cover_preview_url && e.cover_url) ? `srcset="${UI.escape(e.cover_preview_url)} 800w, ${UI.escape(e.cover_url)} 2000w" sizes="(max-width:600px) 300px, 600px"` : ''}
|
||||
alt="Foto" loading="lazy"
|
||||
${e.cover_url ? `onerror="this.src='${UI.escape(e.cover_url)}'"` : ''}>
|
||||
${mediaCount > 1 ? `<span class="diary-card-media-count">${mediaCount}</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,10 @@ window.Page_forum = (() => {
|
|||
const fotoHtml = t.foto_preview
|
||||
? /\.(mp4|mov|webm|m4v|avi)$/i.test(t.foto_preview)
|
||||
? `<div class="forum-card-thumb forum-card-thumb--video" style="display:flex;align-items:center;justify-content:center;background:var(--c-surface-2)">${UI.icon('video-camera')}</div>`
|
||||
: `<img class="forum-card-thumb" src="${_esc(t.foto_preview_url || t.foto_preview)}" alt="" loading="lazy" onerror="this.src='${_esc(t.foto_preview)}'">`
|
||||
: `<img class="forum-card-thumb" src="${_esc(t.foto_preview_url || t.foto_preview)}"
|
||||
${(t.foto_preview_url && t.foto_preview) ? `srcset="${_esc(t.foto_preview_url)} 800w" sizes="120px"` : ''}
|
||||
alt="" loading="lazy"
|
||||
onerror="this.src='${_esc(t.foto_preview)}'">`
|
||||
: '';
|
||||
|
||||
return `
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v437';
|
||||
const CACHE_VERSION = 'by-v438';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue