diff --git a/features/PROJ-77-admin-tabs-dynamic-import.md b/features/PROJ-77-admin-tabs-dynamic-import.md new file mode 100644 index 0000000..1952174 --- /dev/null +++ b/features/PROJ-77-admin-tabs-dynamic-import.md @@ -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: () => , +}) +``` + +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 22–39) 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 = () => ` 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` +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. diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 2945b7f..2537456 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useState } from "react"; +import dynamic from "next/dynamic"; import { useAuth } from "@/hooks/useAuth"; import { useLDAPConfig } from "@/hooks/useLDAPConfig"; import { useTenantLDAPConfig } from "@/hooks/useTenantLDAPConfig"; @@ -18,26 +19,85 @@ import { Navbar } from "@/components/navbar"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -// Tab components -import { DashboardTab } from "@/components/admin/tabs/DashboardTab"; -import { ServicesTab } from "@/components/admin/tabs/ServicesTab"; -import { UsersTab } from "@/components/admin/tabs/UsersTab"; -import { AuditTab } from "@/components/admin/tabs/AuditTab"; -import { ImportTab } from "@/components/admin/tabs/ImportTab"; -import { SecurityTab } from "@/components/admin/tabs/SecurityTab"; -import { LDAPTab } from "@/components/admin/tabs/LDAPTab"; -import { TenantLDAPTab } from "@/components/admin/tabs/TenantLDAPTab"; -import { TenantsTab } from "@/components/admin/tabs/TenantsTab"; -import { CertTab } from "@/components/admin/tabs/CertTab"; -import { ModulesTab } from "@/components/admin/ModulesTab"; -import { IMAPSettingsTab } from "@/components/admin/tabs/IMAPSettingsTab"; -import { RetentionTab } from "@/components/admin/tabs/RetentionTab"; -import { ArchivingRulesTab } from "@/components/admin/tabs/ArchivingRulesTab"; -import { RoutingRulesTab } from "@/components/admin/tabs/RoutingRulesTab"; -import { QuotaTab } from "@/components/admin/tabs/QuotaTab"; -import { SMTPOutTab } from "@/components/admin/tabs/SMTPOutTab"; -import { DSGVOTab } from "@/components/admin/tabs/DSGVOTab"; import { ResetPasswordDialog, DeleteUserDialog } from "@/components/admin/UserDialogs"; +import { TabSkeleton } from "@/components/admin/TabSkeleton"; + +// Tab components — dynamisch nachgeladen (PROJ-77), damit das initiale +// Admin-Bundle nicht den Code aller 19 Tabs enthält. +const loading = () => ; + +const DashboardTab = dynamic( + () => import("@/components/admin/tabs/DashboardTab").then((m) => m.DashboardTab), + { loading } +); +const ServicesTab = dynamic( + () => import("@/components/admin/tabs/ServicesTab").then((m) => m.ServicesTab), + { loading } +); +const UsersTab = dynamic( + () => import("@/components/admin/tabs/UsersTab").then((m) => m.UsersTab), + { loading } +); +const AuditTab = dynamic( + () => import("@/components/admin/tabs/AuditTab").then((m) => m.AuditTab), + { loading } +); +const ImportTab = dynamic( + () => import("@/components/admin/tabs/ImportTab").then((m) => m.ImportTab), + { loading } +); +const SecurityTab = dynamic( + () => import("@/components/admin/tabs/SecurityTab").then((m) => m.SecurityTab), + { loading } +); +const LDAPTab = dynamic( + () => import("@/components/admin/tabs/LDAPTab").then((m) => m.LDAPTab), + { loading } +); +const TenantLDAPTab = dynamic( + () => import("@/components/admin/tabs/TenantLDAPTab").then((m) => m.TenantLDAPTab), + { loading } +); +const TenantsTab = dynamic( + () => import("@/components/admin/tabs/TenantsTab").then((m) => m.TenantsTab), + { loading } +); +const CertTab = dynamic( + () => import("@/components/admin/tabs/CertTab").then((m) => m.CertTab), + { loading } +); +const ModulesTab = dynamic( + () => import("@/components/admin/ModulesTab").then((m) => m.ModulesTab), + { loading } +); +const IMAPSettingsTab = dynamic( + () => import("@/components/admin/tabs/IMAPSettingsTab").then((m) => m.IMAPSettingsTab), + { loading } +); +const RetentionTab = dynamic( + () => import("@/components/admin/tabs/RetentionTab").then((m) => m.RetentionTab), + { loading } +); +const ArchivingRulesTab = dynamic( + () => import("@/components/admin/tabs/ArchivingRulesTab").then((m) => m.ArchivingRulesTab), + { loading } +); +const RoutingRulesTab = dynamic( + () => import("@/components/admin/tabs/RoutingRulesTab").then((m) => m.RoutingRulesTab), + { loading } +); +const QuotaTab = dynamic( + () => import("@/components/admin/tabs/QuotaTab").then((m) => m.QuotaTab), + { loading } +); +const SMTPOutTab = dynamic( + () => import("@/components/admin/tabs/SMTPOutTab").then((m) => m.SMTPOutTab), + { loading } +); +const DSGVOTab = dynamic( + () => import("@/components/admin/tabs/DSGVOTab").then((m) => m.DSGVOTab), + { loading } +); export default function AdminPage() { const { user, loading: authLoading } = useAuth("domain_admin", "/admin/login"); diff --git a/src/components/admin/TabSkeleton.tsx b/src/components/admin/TabSkeleton.tsx new file mode 100644 index 0000000..702ea92 --- /dev/null +++ b/src/components/admin/TabSkeleton.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Skeleton } from "@/components/ui/skeleton"; + +/** + * Einheitlicher Lade-Platzhalter für dynamisch nachgeladene Admin-Tabs. + * Wird von next/dynamic als `loading`-Komponente verwendet (PROJ-77). + */ +export function TabSkeleton() { + return ( +
+ Inhalt wird geladen … + +
+ + + +
+ +
+ ); +}