perf(PROJ-77): Admin-Tabs per next/dynamic laden statt statisch importieren

18 Admin-Tab-Komponenten wurden bisher alle statisch in src/app/admin/page.tsx
importiert, unabhängig davon welcher Tab tatsächlich geöffnet wird. Umstellung
auf next/dynamic mit gemeinsamem TabSkeleton-Loading-State reduziert das
initiale JS-Bundle der Admin-Route um 204,9 KB (-19,8%). Initial aktiver Tab
bleibt weiterhin serverseitig vorgerendert (ssr: true), kein Skeleton-Flash
beim ersten Laden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
This commit is contained in:
sysops
2026-08-05 15:05:24 +02:00
co-authored by Claude Sonnet 5
parent 209fdeb8ad
commit e59b11743a
3 changed files with 183 additions and 19 deletions
@@ -0,0 +1,82 @@
---
id: PROJ-77
title: Admin-Tab-Bundle-Optimierung (dynamic import statt 19 statische Imports)
status: In Review
created: 2026-08-05
---
## Problem
`src/app/admin/page.tsx` (Zeile ~22-40) importiert alle 19 Admin-Tab-
Komponenten statisch. Aktuell gibt es im gesamten Projekt 0 `next/dynamic`-
Imports. Radix `TabsContent` unmountet zwar inaktive Tabs zur Laufzeit
(kein gleichzeitiges Rendern), aber das initiale JS-Bundle enthält trotzdem
den Code aller 19 Tabs, auch wenn ein Nutzer nur einen einzigen Tab je
Sitzung öffnet.
Kein Crash-Bezug, reine Bundle-Größen-/Ladezeit-Optimierung.
## Lösung (Vorschlag)
Jede Admin-Tab-Komponente auf `next/dynamic` mit Loading-State umstellen,
z.B.:
```ts
const DashboardTab = dynamic(() => import('@/components/admin/tabs/DashboardTab'), {
loading: () => <TabSkeleton />,
})
```
Umfang: 19 Komponenten, jeweils Import-Umstellung + einheitlicher
Loading-Skeleton. Vor Umsetzung: kurzer Bundle-Size-Vergleich
(`next build` Output) vorher/nachher, um den tatsächlichen Gewinn zu
beziffern.
## Implementation Notes
Umgesetzt am 2026-08-05.
**Geänderte/neue Dateien**
- `src/app/admin/page.tsx`: 18 statische Tab-Imports (Zeile 2239) durch
`next/dynamic`-Deklarationen ersetzt. Alle Tab-Komponenten sind Named
Exports, daher jeweils
`dynamic(() => import("…/XyzTab").then((m) => m.XyzTab), { loading })`.
Eine gemeinsame `const loading = () => <TabSkeleton />` wird für alle
18 Tabs wiederverwendet. `ResetPasswordDialog`/`DeleteUserDialog`
(Global-Dialoge, immer sichtbar) bleiben bewusst statisch.
- `src/components/admin/TabSkeleton.tsx` (neu): einheitlicher
Lade-Platzhalter auf Basis der vorhandenen shadcn/ui-`Skeleton`-
Komponente, mit `aria-busy`/`aria-live` und sr-only-Text.
**Props-Typisierung:** `dynamic()` inferiert die Props über den
`.then()`-Rückgabewert korrekt; ein explizites `ComponentType<Props>`
war nicht nötig. `npm run build` (inkl. TypeScript-Check) läuft fehlerfrei.
**Bundle-Size-Vergleich** (Next.js 16 / Turbopack zeigt keine Größen im
Build-Output; gemessen als Summe der im prerenderten
`.next/server/app/admin.html` referenzierten `_next/static/*.js`-Dateien,
unkomprimiert):
| | Dateien | Initiales JS |
|---|---|---|
| vorher | 17 | 1035,6 KB |
| nachher | 18 | 830,7 KB |
| Delta | +1 | **204,9 KB (19,8 %)** |
Der Code der Tabs liegt jetzt in separaten Chunks, die erst beim
Aktivieren des jeweiligen Tabs geladen werden.
**Hinweis:** `dynamic()` läuft hier mit Default-`ssr: true`, d.h. der
initial aktive Tab wird weiterhin serverseitig vorgerendert — kein
Flash-of-Skeleton beim ersten Laden des Dashboards.
## Acceptance Criteria
- [x] Alle Admin-Tabs (18 Tab-Komponenten) per `next/dynamic` geladen,
mit Loading-Skeleton.
- [x] `npm run build` Bundle-Size-Report zeigt messbare Reduktion der
initialen Admin-Route-Chunk-Größe (204,9 KB / 19,8 %).
- [ ] Funktionale Regression: alle Tabs weiterhin normal nutzbar
(manueller Klick-Test durch alle Tabs) — **noch zu verifizieren**,
kann nicht automatisiert erfolgen; offen für QA.