From bf6e07a7e46b1083b6479e9664e9e14fe7ef79a4 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Tue, 2 Jun 2026 14:25:57 +0530 Subject: [PATCH] perf(admin): lazy-load route pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the admin route pages in React.lazy with a Suspense boundary in the layout, so each page ships as its own chunk instead of one eager bundle. Page components are now default exports, enabling the canonical lazy(() => import(...)) form. Trims per-page code (~105 KB gz) off the initial/login bundle. App-only change — no SDK or public-API changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/apps/admin/src/App.tsx | 8 +- .../admin/src/pages/admins/AdminsPage.tsx | 2 +- .../src/pages/audit-logs/AuditLogsPage.tsx | 2 +- .../admin/src/pages/invoices/InvoicesPage.tsx | 2 +- .../src/pages/organizations/details/index.tsx | 2 +- .../src/pages/organizations/list/index.tsx | 2 +- web/apps/admin/src/pages/plans/PlansPage.tsx | 2 +- .../src/pages/preferences/PreferencesPage.tsx | 2 +- .../src/pages/products/ProductPricesPage.tsx | 2 +- .../admin/src/pages/products/ProductsPage.tsx | 2 +- web/apps/admin/src/pages/roles/RolesPage.tsx | 2 +- web/apps/admin/src/pages/users/UsersPage.tsx | 2 +- .../admin/src/pages/webhooks/WebhooksPage.tsx | 2 +- web/apps/admin/src/routes.tsx | 90 +++++++++++++------ 14 files changed, 80 insertions(+), 42 deletions(-) diff --git a/web/apps/admin/src/App.tsx b/web/apps/admin/src/App.tsx index 541a2be6a..4d2720e35 100644 --- a/web/apps/admin/src/App.tsx +++ b/web/apps/admin/src/App.tsx @@ -1,16 +1,22 @@ import { Flex } from "@raystack/apsara"; +import { Suspense } from "react"; import { Outlet } from "react-router-dom"; import "@raystack/apsara/normalize.css"; import "@raystack/apsara/style.css"; import "./App.css"; import IAMSidebar from "./components/Sidebar"; +import LoadingState from "./components/states/Loading"; function App() { return (
- + {/* Boundary for the lazily-loaded route pages. Sits inside the layout + so the sidebar stays mounted while a route chunk loads. */} + }> + +
); diff --git a/web/apps/admin/src/pages/admins/AdminsPage.tsx b/web/apps/admin/src/pages/admins/AdminsPage.tsx index 7b8140ebf..af33228c6 100644 --- a/web/apps/admin/src/pages/admins/AdminsPage.tsx +++ b/web/apps/admin/src/pages/admins/AdminsPage.tsx @@ -2,7 +2,7 @@ import { AdminsView, useAdminPaths } from "@raystack/frontier/admin"; import { useNavigate } from "react-router-dom"; import AdminsIcon from "~/assets/icons/admins.svg?react"; -export function AdminsPage() { +export default function AdminsPage() { const navigate = useNavigate(); const paths = useAdminPaths(); diff --git a/web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx b/web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx index 92214e7a4..a89e21266 100644 --- a/web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx +++ b/web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx @@ -7,7 +7,7 @@ import type { RQLExportRequest, RQLRequest } from "@raystack/proton/frontier"; const adminClient = clients.admin({ useBinary: true }); -export function AuditLogsPage() { +export default function AuditLogsPage() { const navigate = useNavigate(); const onExportCsv = useCallback(async (query: RQLRequest) => { await exportCsvFromStream( diff --git a/web/apps/admin/src/pages/invoices/InvoicesPage.tsx b/web/apps/admin/src/pages/invoices/InvoicesPage.tsx index 32501e84c..8ecd39a98 100644 --- a/web/apps/admin/src/pages/invoices/InvoicesPage.tsx +++ b/web/apps/admin/src/pages/invoices/InvoicesPage.tsx @@ -1,5 +1,5 @@ import { InvoicesView } from "@raystack/frontier/admin"; -export function InvoicesPage() { +export default function InvoicesPage() { return ; } diff --git a/web/apps/admin/src/pages/organizations/details/index.tsx b/web/apps/admin/src/pages/organizations/details/index.tsx index ce1578335..c7b84e8e7 100644 --- a/web/apps/admin/src/pages/organizations/details/index.tsx +++ b/web/apps/admin/src/pages/organizations/details/index.tsx @@ -12,7 +12,7 @@ async function loadCountries(): Promise { return (data.default as { name: string }[]).map((c) => c.name); } -export function OrganizationDetailsPage() { +export default function OrganizationDetailsPage() { const { organizationId } = useParams<{ organizationId: string }>(); const location = useLocation(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/pages/organizations/list/index.tsx b/web/apps/admin/src/pages/organizations/list/index.tsx index 6d5e0079d..83e0dd26e 100644 --- a/web/apps/admin/src/pages/organizations/list/index.tsx +++ b/web/apps/admin/src/pages/organizations/list/index.tsx @@ -15,7 +15,7 @@ async function loadCountries(): Promise { return (data.default as { name: string }[]).map((c) => c.name); } -export function OrganizationListPage() { +export default function OrganizationListPage() { const navigate = useNavigate(); const { config } = useContext(AppContext); const paths = useAdminPaths(); diff --git a/web/apps/admin/src/pages/plans/PlansPage.tsx b/web/apps/admin/src/pages/plans/PlansPage.tsx index ce78058b8..3a5bbef8d 100644 --- a/web/apps/admin/src/pages/plans/PlansPage.tsx +++ b/web/apps/admin/src/pages/plans/PlansPage.tsx @@ -2,7 +2,7 @@ import { PlansView } from "@raystack/frontier/admin"; import { useNavigate, useParams } from "react-router-dom"; import PlansIcon from "~/assets/icons/plans.svg?react"; -export function PlansPage() { +export default function PlansPage() { const { planId } = useParams(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/pages/preferences/PreferencesPage.tsx b/web/apps/admin/src/pages/preferences/PreferencesPage.tsx index 43c716895..2619359c6 100644 --- a/web/apps/admin/src/pages/preferences/PreferencesPage.tsx +++ b/web/apps/admin/src/pages/preferences/PreferencesPage.tsx @@ -2,7 +2,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { PreferencesView } from "@raystack/frontier/admin"; import PreferencesIcon from "~/assets/icons/preferences.svg?react"; -export function PreferencesPage() { +export default function PreferencesPage() { const { name } = useParams(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/pages/products/ProductPricesPage.tsx b/web/apps/admin/src/pages/products/ProductPricesPage.tsx index ea84284c6..a2b8e2f23 100644 --- a/web/apps/admin/src/pages/products/ProductPricesPage.tsx +++ b/web/apps/admin/src/pages/products/ProductPricesPage.tsx @@ -1,7 +1,7 @@ import { ProductPricesView } from "@raystack/frontier/admin"; import { useParams, useNavigate } from "react-router-dom"; -export function ProductPricesPage() { +export default function ProductPricesPage() { const { productId } = useParams(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/pages/products/ProductsPage.tsx b/web/apps/admin/src/pages/products/ProductsPage.tsx index a36dd0bab..460b2abda 100644 --- a/web/apps/admin/src/pages/products/ProductsPage.tsx +++ b/web/apps/admin/src/pages/products/ProductsPage.tsx @@ -2,7 +2,7 @@ import { ProductsView } from "@raystack/frontier/admin"; import { useParams, useNavigate } from "react-router-dom"; import ProductsIcon from "~/assets/icons/products.svg?react"; -export function ProductsPage() { +export default function ProductsPage() { const { productId } = useParams(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/pages/roles/RolesPage.tsx b/web/apps/admin/src/pages/roles/RolesPage.tsx index ae4b8248d..0a06079c9 100644 --- a/web/apps/admin/src/pages/roles/RolesPage.tsx +++ b/web/apps/admin/src/pages/roles/RolesPage.tsx @@ -2,7 +2,7 @@ import { RolesView } from "@raystack/frontier/admin"; import { useParams, useNavigate } from "react-router-dom"; import RolesIcon from "~/assets/icons/roles.svg?react"; -export function RolesPage() { +export default function RolesPage() { const { roleId } = useParams(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/pages/users/UsersPage.tsx b/web/apps/admin/src/pages/users/UsersPage.tsx index b82a684fe..7a105cd95 100644 --- a/web/apps/admin/src/pages/users/UsersPage.tsx +++ b/web/apps/admin/src/pages/users/UsersPage.tsx @@ -6,7 +6,7 @@ import { exportCsvFromStream } from "~/utils/helper"; const adminClient = clients.admin({ useBinary: true }); -export function UsersPage() { +export default function UsersPage() { const { userId } = useParams(); const navigate = useNavigate(); const location = useLocation(); diff --git a/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx b/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx index 510d6c0ab..b919da73b 100644 --- a/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx +++ b/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx @@ -4,7 +4,7 @@ import { WebhooksView } from "@raystack/frontier/admin"; import { AppContext } from "~/contexts/App"; import WebhooksIcon from "~/assets/icons/webhooks.svg?react"; -export function WebhooksPage() { +export default function WebhooksPage() { const { config } = useContext(AppContext); const { webhookId } = useParams(); const navigate = useNavigate(); diff --git a/web/apps/admin/src/routes.tsx b/web/apps/admin/src/routes.tsx index e4fd66bb8..1cf1327ff 100644 --- a/web/apps/admin/src/routes.tsx +++ b/web/apps/admin/src/routes.tsx @@ -1,43 +1,75 @@ import * as R from "ramda"; -import { memo, useContext } from "react"; +import { lazy, memo, useContext } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import LoadingState from "./components/states/Loading"; import UnauthorizedState from "./components/states/Unauthorized"; +// Eager: the app shell and the unauthenticated flow. Keeping these in the +// initial bundle means the login screen paints instantly. import App from "./App"; -import { PlansPage } from "./pages/plans/PlansPage"; import Login from "./containers/login"; import MagicLink from "./containers/magiclink"; - -import { PreferencesPage } from "./pages/preferences/PreferencesPage"; -import { ProductsPage } from "./pages/products/ProductsPage"; -import { ProductPricesPage } from "./pages/products/ProductPricesPage"; - -import { RolesPage } from "./pages/roles/RolesPage"; - -import { AppContext } from "./contexts/App"; -import { AdminsPage } from "./pages/admins/AdminsPage"; -import { WebhooksPage } from "./pages/webhooks/WebhooksPage"; import AuthLayout from "./layout/auth"; -import { OrganizationListPage } from "./pages/organizations/list"; -import { OrganizationDetailsPage } from "./pages/organizations/details"; -import { - OrganizationSecurity, - OrganizationMembersView, - OrganizationProjectsView, - OrganizationInvoicesView, - OrganizationTokensView, - OrganizationApisView, - OrganizationPatView, - useAdminPaths, -} from "@raystack/frontier/admin"; - -import { UsersPage } from "./pages/users/UsersPage"; - -import { InvoicesPage } from "./pages/invoices/InvoicesPage"; -import { AuditLogsPage } from "./pages/audit-logs/AuditLogsPage"; +import { AppContext } from "./contexts/App"; +// useAdminPaths is a hook called during render, so it must stay a static import. +import { useAdminPaths } from "@raystack/frontier/admin"; + +// Lazily-loaded route pages — each becomes its own async chunk, so the heavy +// admin code stays out of the initial/unauthenticated bundle. The pages are +// default exports, so React.lazy imports them directly (per the React docs). +const PlansPage = lazy(() => import("./pages/plans/PlansPage")); +const PreferencesPage = lazy(() => import("./pages/preferences/PreferencesPage")); +const ProductsPage = lazy(() => import("./pages/products/ProductsPage")); +const ProductPricesPage = lazy(() => import("./pages/products/ProductPricesPage")); +const RolesPage = lazy(() => import("./pages/roles/RolesPage")); +const AdminsPage = lazy(() => import("./pages/admins/AdminsPage")); +const WebhooksPage = lazy(() => import("./pages/webhooks/WebhooksPage")); +const OrganizationListPage = lazy(() => import("./pages/organizations/list")); +const OrganizationDetailsPage = lazy(() => import("./pages/organizations/details")); +const UsersPage = lazy(() => import("./pages/users/UsersPage")); +const InvoicesPage = lazy(() => import("./pages/invoices/InvoicesPage")); +const AuditLogsPage = lazy(() => import("./pages/audit-logs/AuditLogsPage")); + +// Organization detail child views come from the SDK barrel. Each lazy() shares +// the same `@raystack/frontier/admin` chunk (the module promise is cached), so +// the admin SDK loads once on the first org route and stays cached after. +const OrganizationSecurity = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationSecurity, + })), +); +const OrganizationMembersView = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationMembersView, + })), +); +const OrganizationProjectsView = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationProjectsView, + })), +); +const OrganizationInvoicesView = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationInvoicesView, + })), +); +const OrganizationTokensView = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationTokensView, + })), +); +const OrganizationApisView = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationApisView, + })), +); +const OrganizationPatView = lazy(() => + import("@raystack/frontier/admin").then((m) => ({ + default: m.OrganizationPatView, + })), +); export default memo(function AppRoutes() { const { isAdmin, isLoading, user } = useContext(AppContext);