Code-split feature pages and admin shell with React.lazy
The dashboard (recharts, ~120 kB gzip), tracing, troubleshoot, actions and bootstrap wizard now load on demand, and the admin panel is split out of the entry chunk so anonymous visitors only download the login page (~134 kB gzip instead of ~557 kB).
This commit is contained in:
+5
-1
@@ -4,14 +4,18 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Suspense } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
||||||
|
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Outlet />
|
<Suspense fallback={<LoadingFallback fullScreen />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||||
|
*
|
||||||
|
* 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 (
|
||||||
|
<div className={`flex items-center justify-center ${fullScreen ? 'min-h-screen' : 'p-8'}`}>
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
<p className="text-muted-foreground">{t('common.loading')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* 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 { useSchemaStore } from '@/stores/schemaStore';
|
||||||
import { useCacheStore } from '@/stores/cacheStore';
|
import { useCacheStore } from '@/stores/cacheStore';
|
||||||
import { useAccountStore } from '@/stores/accountStore';
|
import { useAccountStore } from '@/stores/accountStore';
|
||||||
@@ -12,11 +12,23 @@ import { resolveObject } from '@/lib/schemaResolver';
|
|||||||
import { DynamicList } from '@/components/lists/DynamicList';
|
import { DynamicList } from '@/components/lists/DynamicList';
|
||||||
import { DynamicForm } from '@/components/forms/DynamicForm';
|
import { DynamicForm } from '@/components/forms/DynamicForm';
|
||||||
import { DynamicViewPage } from '@/components/views/DynamicViewPage';
|
import { DynamicViewPage } from '@/components/views/DynamicViewPage';
|
||||||
import { DashboardView } from '@/features/dashboard/components/DashboardView';
|
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||||
import { DeliveryTracePage } from '@/features/troubleshoot/DeliveryTracePage';
|
|
||||||
import { LiveTracingPage } from '@/features/tracing/components/LiveTracingPage';
|
// Heavy or rarely used feature pages are code-split so the initial bundle
|
||||||
import { TraceDetailView } from '@/features/tracing/components/TraceDetailView';
|
// stays small (the dashboard pulls in recharts, ~150 kB gzipped on its own).
|
||||||
import { ActionPage } from '@/features/actions/ActionPage';
|
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 {
|
interface MainContentProps {
|
||||||
viewName?: string;
|
viewName?: string;
|
||||||
@@ -25,6 +37,14 @@ interface MainContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MainContent({ viewName, id, section }: MainContentProps) {
|
export function MainContent({ viewName, id, section }: MainContentProps) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
|
<MainContentView viewName={viewName} id={id} section={section} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MainContentView({ viewName, id, section }: MainContentProps) {
|
||||||
const schema = useSchemaStore((s) => s.schema);
|
const schema = useSchemaStore((s) => s.schema);
|
||||||
const invalidateAllObjectLists = useCacheStore((s) => s.invalidateAllObjectLists);
|
const invalidateAllObjectLists = useCacheStore((s) => s.invalidateAllObjectLists);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,8 +12,8 @@ import './index.css';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import LoginPage from './pages/LoginPage';
|
import LoginPage from './pages/LoginPage';
|
||||||
import OAuthCallback from './pages/OAuthCallback';
|
import OAuthCallback from './pages/OAuthCallback';
|
||||||
import AdminPanel from './pages/AdminPanel';
|
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from './pages/NotFound';
|
||||||
|
import { AdminPanel } from './pages/AdminPanel.lazy';
|
||||||
import { ProtectedRoute } from './components/layout/ProtectedRoute';
|
import { ProtectedRoute } from './components/layout/ProtectedRoute';
|
||||||
import { getBasePath } from './lib/basePath';
|
import { getBasePath } from './lib/basePath';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||||
|
*
|
||||||
|
* 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'));
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* 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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuthStore } from '@/stores/authStore';
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
@@ -17,7 +17,7 @@ import { TopBar } from '@/components/layout/TopBar';
|
|||||||
import { Sidebar } from '@/components/layout/Sidebar';
|
import { Sidebar } from '@/components/layout/Sidebar';
|
||||||
import { MainContent } from '@/components/layout/MainContent';
|
import { MainContent } from '@/components/layout/MainContent';
|
||||||
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
||||||
import { BootstrapWizard } from '@/components/bootstrap/BootstrapWizard';
|
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||||
import {
|
import {
|
||||||
findFirstAccessibleLinkInLayout,
|
findFirstAccessibleLinkInLayout,
|
||||||
findFirstVisibleLinkInLayout,
|
findFirstVisibleLinkInLayout,
|
||||||
@@ -27,6 +27,11 @@ import {
|
|||||||
import { usePermissions } from '@/hooks/usePermissions';
|
import { usePermissions } from '@/hooks/usePermissions';
|
||||||
import { Loader2 } from 'lucide-react';
|
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() {
|
export default function AdminPanel() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -237,7 +242,9 @@ export default function AdminPanel() {
|
|||||||
if (isBootstrapMode) {
|
if (isBootstrapMode) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<BootstrapWizard />
|
<Suspense fallback={<LoadingFallback fullScreen />}>
|
||||||
|
<BootstrapWizard />
|
||||||
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user