feat(App): implement code-splitting for improved performance and user experience
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 56s

- Refactored the App component to utilize React's lazy loading for page components, enhancing load times and performance.
- Introduced a fallback UI with a spinner during component loading, improving user feedback during navigation.
- Updated the AuthContext to use useCallback and useMemo for optimized performance in login and logout functions, reducing unnecessary re-renders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-13 22:10:02 +02:00
parent 85163ad440
commit 1c268555f6
3 changed files with 107 additions and 46 deletions

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { Suspense, lazy } from 'react'
import { import {
RouterProvider, RouterProvider,
createBrowserRouter, createBrowserRouter,
@ -12,45 +12,66 @@ import { ToastProvider } from './context/ToastContext'
import { OrgInboxProvider, useOrgInbox } from './context/OrgInboxContext' import { OrgInboxProvider, useOrgInbox } from './context/OrgInboxContext'
import DesktopSidebar from './components/DesktopSidebar' import DesktopSidebar from './components/DesktopSidebar'
import { getMainNavItems } from './config/appNav' import { getMainNavItems } from './config/appNav'
import LoginPage from './pages/LoginPage'
import VerifyPage from './pages/VerifyPage'
import Dashboard from './pages/Dashboard'
import AccountSettingsPage from './pages/AccountSettingsPage'
import SettingsSystemInfoPage from './pages/SettingsSystemInfoPage'
import ExercisesListPage from './pages/ExercisesListPage'
import ExerciseDetailPage from './pages/ExerciseDetailPage'
import ExerciseFormPage from './pages/ExerciseFormPage'
import ClubsPage from './pages/ClubsPage'
import InboxPage from './pages/InboxPage'
import SkillsPage from './pages/SkillsPage'
import TrainingPlanningPage from './pages/TrainingPlanningPage'
import TrainingFrameworkProgramsListPage from './pages/TrainingFrameworkProgramsListPage'
import TrainingFrameworkProgramEditPage from './pages/TrainingFrameworkProgramEditPage'
import TrainingModulesListPage from './pages/TrainingModulesListPage'
import TrainingModuleEditPage from './pages/TrainingModuleEditPage'
import TrainingUnitRunPage from './pages/TrainingUnitRunPage'
import TrainingCoachPage from './pages/TrainingCoachPage'
import AdminCatalogsPage from './pages/AdminCatalogsPage'
import AdminHierarchyPage from './pages/AdminHierarchyPage'
import AdminMaturityModelsPage from './pages/AdminMaturityModelsPage'
import TrainerContextsPage from './pages/TrainerContextsPage'
import MediaWikiImportPage from './pages/MediaWikiImportPage'
import AdminUsersPage from './pages/AdminUsersPage'
import AdminHomeRedirect from './components/AdminHomeRedirect' import AdminHomeRedirect from './components/AdminHomeRedirect'
import PlatformAdminRoute from './components/PlatformAdminRoute' import PlatformAdminRoute from './components/PlatformAdminRoute'
import MediaLibraryPage from './pages/MediaLibraryPage'
import LegalPage from './pages/LegalPage'
import AdminLegalDocumentsPage from './pages/AdminLegalDocumentsPage'
import SettingsLegalPage from './pages/SettingsLegalPage'
import ActiveClubSwitcher from './components/ActiveClubSwitcher' import ActiveClubSwitcher from './components/ActiveClubSwitcher'
import InactiveMembershipBanner from './components/InactiveMembershipBanner' import InactiveMembershipBanner from './components/InactiveMembershipBanner'
import './app.css' import './app.css'
const LoginPage = lazy(() => import('./pages/LoginPage'))
const VerifyPage = lazy(() => import('./pages/VerifyPage'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const AccountSettingsPage = lazy(() => import('./pages/AccountSettingsPage'))
const SettingsSystemInfoPage = lazy(() => import('./pages/SettingsSystemInfoPage'))
const ExercisesListPage = lazy(() => import('./pages/ExercisesListPage'))
const ExerciseDetailPage = lazy(() => import('./pages/ExerciseDetailPage'))
const ExerciseFormPage = lazy(() => import('./pages/ExerciseFormPage'))
const ClubsPage = lazy(() => import('./pages/ClubsPage'))
const InboxPage = lazy(() => import('./pages/InboxPage'))
const SkillsPage = lazy(() => import('./pages/SkillsPage'))
const TrainingPlanningPage = lazy(() => import('./pages/TrainingPlanningPage'))
const TrainingFrameworkProgramsListPage = lazy(() =>
import('./pages/TrainingFrameworkProgramsListPage'),
)
const TrainingFrameworkProgramEditPage = lazy(() =>
import('./pages/TrainingFrameworkProgramEditPage'),
)
const TrainingModulesListPage = lazy(() => import('./pages/TrainingModulesListPage'))
const TrainingModuleEditPage = lazy(() => import('./pages/TrainingModuleEditPage'))
const TrainingUnitRunPage = lazy(() => import('./pages/TrainingUnitRunPage'))
const TrainingCoachPage = lazy(() => import('./pages/TrainingCoachPage'))
const AdminCatalogsPage = lazy(() => import('./pages/AdminCatalogsPage'))
const AdminHierarchyPage = lazy(() => import('./pages/AdminHierarchyPage'))
const AdminMaturityModelsPage = lazy(() => import('./pages/AdminMaturityModelsPage'))
const TrainerContextsPage = lazy(() => import('./pages/TrainerContextsPage'))
const MediaWikiImportPage = lazy(() => import('./pages/MediaWikiImportPage'))
const AdminUsersPage = lazy(() => import('./pages/AdminUsersPage'))
const MediaLibraryPage = lazy(() => import('./pages/MediaLibraryPage'))
const LegalPage = lazy(() => import('./pages/LegalPage'))
const AdminLegalDocumentsPage = lazy(() => import('./pages/AdminLegalDocumentsPage'))
const SettingsLegalPage = lazy(() => import('./pages/SettingsLegalPage'))
/** Shield „Admin“: nur Super-Admin (global). Vereinsorga: Vereine → Mitglieder. */ /** Shield „Admin“: nur Super-Admin (global). Vereinsorga: Vereine → Mitglieder. */
function computeShowAdminNav(currentUser) { function computeShowAdminNav(currentUser) {
return currentUser?.role === 'superadmin' return currentUser?.role === 'superadmin'
} }
function AppRouteFallback() {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)',
}}
>
<div className="spinner"></div>
</div>
)
}
// Bottom Navigation (Mobile) // Bottom Navigation (Mobile)
function Nav({ showAdminNav }) { function Nav({ showAdminNav }) {
const { canAccessOrgInbox, inboxCount } = useOrgInbox() const { canAccessOrgInbox, inboxCount } = useOrgInbox()
@ -270,7 +291,9 @@ function App() {
return ( return (
<AuthProvider> <AuthProvider>
<ToastProvider> <ToastProvider>
<RouterProvider router={appRouter} /> <Suspense fallback={<AppRouteFallback />}>
<RouterProvider router={appRouter} />
</Suspense>
</ToastProvider> </ToastProvider>
</AuthProvider> </AuthProvider>
) )

View File

@ -1,4 +1,12 @@
import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react' import {
createContext,
useContext,
useState,
useEffect,
useCallback,
useMemo,
useRef,
} from 'react'
import api, { ACTIVE_CLUB_STORAGE_KEY } from '../utils/api' import api, { ACTIVE_CLUB_STORAGE_KEY } from '../utils/api'
import { activeClubMemberships } from '../utils/activeClub' import { activeClubMemberships } from '../utils/activeClub'
@ -94,7 +102,7 @@ export function AuthProvider({ children }) {
}, []) }, [])
/** Fallback, falls ohne checkAuth gesetzt wird (Legacy / Token-Injektion) */ /** Fallback, falls ohne checkAuth gesetzt wird (Legacy / Token-Injektion) */
const login = (payload) => { const login = useCallback((payload) => {
if (payload?.profile != null) { if (payload?.profile != null) {
syncStoredActiveClub(payload.profile) syncStoredActiveClub(payload.profile)
setUser(payload.profile) setUser(payload.profile)
@ -112,9 +120,9 @@ export function AuthProvider({ children }) {
return return
} }
setUser(payload) setUser(payload)
} }, [])
const logout = () => { const logout = useCallback(() => {
setUser(null) setUser(null)
localStorage.removeItem('authToken') localStorage.removeItem('authToken')
localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY) localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY)
@ -123,17 +131,20 @@ export function AuthProvider({ children }) {
sessionStorage.removeItem(key) sessionStorage.removeItem(key)
} }
} }
} }, [])
const value = { const value = useMemo(
user, () => ({
isAuthenticated: !!user, user,
loading, isAuthenticated: !!user,
login, loading,
logout, login,
checkAuth, logout,
setActiveClub, checkAuth,
} setActiveClub,
}),
[user, loading, login, logout, checkAuth, setActiveClub],
)
return ( return (
<AuthContext.Provider value={value}> <AuthContext.Provider value={value}>

View File

@ -9,6 +9,33 @@ export default defineConfig({
}, },
build: { build: {
outDir: 'dist', outDir: 'dist',
sourcemap: false sourcemap: false,
} rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes('node_modules')) return
if (id.includes('jspdf')) return 'vendor-pdf'
if (id.includes('lucide-react')) return 'vendor-icons'
if (
id.includes('react-markdown') ||
id.includes('/marked/') ||
id.includes('remark-') ||
id.includes('mdast') ||
id.includes('micromark') ||
id.includes('unist')
) {
return 'vendor-markdown'
}
if (id.includes('react-router')) return 'vendor-router'
if (
/[/\\]node_modules[/\\]react-dom[/\\]/.test(id) ||
/[/\\]node_modules[/\\]react[/\\]/.test(id) ||
/[/\\]node_modules[/\\]scheduler[/\\]/.test(id)
) {
return 'vendor-react'
}
},
},
},
},
}) })