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.
+79 -19
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import dynamic from "next/dynamic";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { useLDAPConfig } from "@/hooks/useLDAPConfig"; import { useLDAPConfig } from "@/hooks/useLDAPConfig";
import { useTenantLDAPConfig } from "@/hooks/useTenantLDAPConfig"; import { useTenantLDAPConfig } from "@/hooks/useTenantLDAPConfig";
@@ -18,26 +19,85 @@ import { Navbar } from "@/components/navbar";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 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 { 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 = () => <TabSkeleton />;
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() { export default function AdminPage() {
const { user, loading: authLoading } = useAuth("domain_admin", "/admin/login"); const { user, loading: authLoading } = useAuth("domain_admin", "/admin/login");
+22
View File
@@ -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 (
<div className="mt-4 space-y-4" aria-busy="true" aria-live="polite">
<span className="sr-only">Inhalt wird geladen </span>
<Skeleton className="h-8 w-48" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-2/3" />
</div>
<Skeleton className="h-48 w-full" />
</div>
);
}