diff --git a/src/App.tsx b/src/App.tsx index 57fc917..1c815e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,14 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +import { Suspense } from 'react'; import { Outlet } from 'react-router-dom'; import { ErrorBoundary } from '@/components/layout/ErrorBoundary'; +import { LoadingFallback } from '@/components/common/LoadingFallback'; import { Toaster } from '@/components/ui/toaster'; export default function App() { return ( - + }> + + ); diff --git a/src/components/common/LoadingFallback.tsx b/src/components/common/LoadingFallback.tsx new file mode 100644 index 0000000..e7b107e --- /dev/null +++ b/src/components/common/LoadingFallback.tsx @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +import { Loader2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +interface LoadingFallbackProps { + fullScreen?: boolean; +} + +export function LoadingFallback({ fullScreen }: LoadingFallbackProps) { + const { t } = useTranslation(); + return ( +
+
+ +

{t('common.loading')}

+
+
+ ); +} diff --git a/src/components/layout/MainContent.tsx b/src/components/layout/MainContent.tsx index 4b04c83..7706f2d 100644 --- a/src/components/layout/MainContent.tsx +++ b/src/components/layout/MainContent.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -import { useEffect } from 'react'; +import { lazy, Suspense, useEffect } from 'react'; import { useSchemaStore } from '@/stores/schemaStore'; import { useCacheStore } from '@/stores/cacheStore'; import { useAccountStore } from '@/stores/accountStore'; @@ -12,11 +12,23 @@ import { resolveObject } from '@/lib/schemaResolver'; import { DynamicList } from '@/components/lists/DynamicList'; import { DynamicForm } from '@/components/forms/DynamicForm'; import { DynamicViewPage } from '@/components/views/DynamicViewPage'; -import { DashboardView } from '@/features/dashboard/components/DashboardView'; -import { DeliveryTracePage } from '@/features/troubleshoot/DeliveryTracePage'; -import { LiveTracingPage } from '@/features/tracing/components/LiveTracingPage'; -import { TraceDetailView } from '@/features/tracing/components/TraceDetailView'; -import { ActionPage } from '@/features/actions/ActionPage'; +import { LoadingFallback } from '@/components/common/LoadingFallback'; + +// Heavy or rarely used feature pages are code-split so the initial bundle +// stays small (the dashboard pulls in recharts, ~150 kB gzipped on its own). +const DashboardView = lazy(() => + import('@/features/dashboard/components/DashboardView').then((m) => ({ default: m.DashboardView })), +); +const DeliveryTracePage = lazy(() => + import('@/features/troubleshoot/DeliveryTracePage').then((m) => ({ default: m.DeliveryTracePage })), +); +const LiveTracingPage = lazy(() => + import('@/features/tracing/components/LiveTracingPage').then((m) => ({ default: m.LiveTracingPage })), +); +const TraceDetailView = lazy(() => + import('@/features/tracing/components/TraceDetailView').then((m) => ({ default: m.TraceDetailView })), +); +const ActionPage = lazy(() => import('@/features/actions/ActionPage').then((m) => ({ default: m.ActionPage }))); interface MainContentProps { viewName?: string; @@ -25,6 +37,14 @@ interface MainContentProps { } export function MainContent({ viewName, id, section }: MainContentProps) { + return ( + }> + + + ); +} + +function MainContentView({ viewName, id, section }: MainContentProps) { const schema = useSchemaStore((s) => s.schema); const invalidateAllObjectLists = useCacheStore((s) => s.invalidateAllObjectLists); diff --git a/src/main.tsx b/src/main.tsx index 5cfa5ce..06656fa 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,8 +12,8 @@ import './index.css'; import App from './App'; import LoginPage from './pages/LoginPage'; import OAuthCallback from './pages/OAuthCallback'; -import AdminPanel from './pages/AdminPanel'; import NotFound from './pages/NotFound'; +import { AdminPanel } from './pages/AdminPanel.lazy'; import { ProtectedRoute } from './components/layout/ProtectedRoute'; import { getBasePath } from './lib/basePath'; diff --git a/src/pages/AdminPanel.lazy.tsx b/src/pages/AdminPanel.lazy.tsx new file mode 100644 index 0000000..03a6a3d --- /dev/null +++ b/src/pages/AdminPanel.lazy.tsx @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +import { lazy } from 'react'; + +// The admin panel (and everything reachable from it) is split out of the +// entry chunk so anonymous visitors only download the login page. +// Kept in a dedicated file so main.tsx stays component-free for fast refresh. +export const AdminPanel = lazy(() => import('./AdminPanel')); diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx index 76e80ac..7959abd 100644 --- a/src/pages/AdminPanel.tsx +++ b/src/pages/AdminPanel.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -import { useEffect, useMemo, useState } from 'react'; +import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/stores/authStore'; @@ -17,7 +17,7 @@ import { TopBar } from '@/components/layout/TopBar'; import { Sidebar } from '@/components/layout/Sidebar'; import { MainContent } from '@/components/layout/MainContent'; import { ErrorBoundary } from '@/components/layout/ErrorBoundary'; -import { BootstrapWizard } from '@/components/bootstrap/BootstrapWizard'; +import { LoadingFallback } from '@/components/common/LoadingFallback'; import { findFirstAccessibleLinkInLayout, findFirstVisibleLinkInLayout, @@ -27,6 +27,11 @@ import { import { usePermissions } from '@/hooks/usePermissions'; import { Loader2 } from 'lucide-react'; +// Bootstrap mode is a rare first-run flow; keep it out of the main chunk. +const BootstrapWizard = lazy(() => + import('@/components/bootstrap/BootstrapWizard').then((m) => ({ default: m.BootstrapWizard })), +); + export default function AdminPanel() { const { t } = useTranslation(); const navigate = useNavigate(); @@ -237,7 +242,9 @@ export default function AdminPanel() { if (isBootstrapMode) { return ( - + }> + + ); }