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:
co-authored by
Claude Sonnet 5
parent
209fdeb8ad
commit
e59b11743a
@@ -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 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 = () => <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
@@ -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 = () => <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() {
|
||||
const { user, loading: authLoading } = useAuth("domain_admin", "/admin/login");
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user