perf(admin): lazy-load route pages#1666
Conversation
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) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR implements code-splitting for admin route pages by introducing a Suspense boundary in the layout, converting page components to default exports, and configuring lazy loading in the routes module. Eleven page components and seven organization nested views transition from eager to lazy imports while maintaining the sidebar visibility during route chunk load. ChangesCode-splitting route pages
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for CI Build 26809372529Coverage remained the same at 43.06%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/apps/admin/src/pages/organizations/details/index.tsx (1)
66-66:⚠️ Potential issue | 🟠 Major | ⚡ Quick winWrap the nested
<Outlet />in a local Suspense boundary to avoid replacing the whole org details shell on lazy-tab load.
routes.tsxlazy-loads the org child views, andOrganizationDetailsViewrenders{children}directly (no Suspense boundary). Without a local boundary around the nested<Outlet />, the suspension bubbles to the top-level Suspense inApp.tsx, which swaps out the entire routed content (including the org header/tabs) withLoadingStateuntil the chunk arrives. A local boundary confines the fallback to the nested tab area.♻️ Proposed local boundary
-import { useCallback, useContext, useEffect, useState } from 'react'; +import { Suspense, useCallback, useContext, useEffect, useState } from 'react'; +import LoadingState from '~/components/states/Loading';- <Outlet /> + <Suspense fallback={<LoadingState />}> + <Outlet /> + </Suspense>
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c6fe09ed-4188-4078-b5f8-d949fa6df30f
📒 Files selected for processing (14)
web/apps/admin/src/App.tsxweb/apps/admin/src/pages/admins/AdminsPage.tsxweb/apps/admin/src/pages/audit-logs/AuditLogsPage.tsxweb/apps/admin/src/pages/invoices/InvoicesPage.tsxweb/apps/admin/src/pages/organizations/details/index.tsxweb/apps/admin/src/pages/organizations/list/index.tsxweb/apps/admin/src/pages/plans/PlansPage.tsxweb/apps/admin/src/pages/preferences/PreferencesPage.tsxweb/apps/admin/src/pages/products/ProductPricesPage.tsxweb/apps/admin/src/pages/products/ProductsPage.tsxweb/apps/admin/src/pages/roles/RolesPage.tsxweb/apps/admin/src/pages/users/UsersPage.tsxweb/apps/admin/src/pages/webhooks/WebhooksPage.tsxweb/apps/admin/src/routes.tsx
| 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, | ||
| })), | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the org details page for a nested Suspense around its Outlet.
fd -t f 'index.tsx' web/apps/admin/src/pages/organizations/details --exec rg -nP 'Suspense|Outlet|fallback' {}Repository: raystack/frontier
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find App.tsx top-level Suspense around routed Outlet =="
if [ -f web/apps/admin/src/App.tsx ]; then
rg -n --context 3 "Suspense|Outlet|LoadingState" web/apps/admin/src/App.tsx || true
else
echo "App.tsx not found at web/apps/admin/src/App.tsx"
fi
echo
echo "== Inspect organizations/details page wrapper (web/apps/admin/src/pages/organizations/details) =="
fd -t f 'index.tsx' web/apps/admin/src/pages/organizations/details --exec rg -n --context 3 "Suspense|Outlet|fallback|OrganizationDetails" {} || true
echo
echo "== Inspect OrganizationDetailsView implementation (web/sdk/admin/views/organizations/details) =="
fd -t f 'index.tsx' web/sdk/admin/views/organizations/details --exec rg -n --context 3 "Suspense|children|fallback|Outlet" {} || true
echo
echo "== Locate where {children} is rendered in sdk details view =="
fd -t f 'index.tsx' web/sdk/admin/views/organizations/details --exec rg -n --context 2 "\\{children\\}" {} || trueRepository: raystack/frontier
Length of output: 4099
Add a nested Suspense boundary around the org-detail tab Outlet/children.
web/apps/admin/src/pages/organizations/details/index.tsx passes <Outlet /> into OrganizationDetailsView as children, and web/sdk/admin/views/organizations/details/index.tsx renders {children} directly with no local <Suspense fallback>. When a lazy org-detail tab suspends, it bubbles to the top-level <Suspense fallback={<LoadingState />}> in web/apps/admin/src/App.tsx, which replaces the whole org-details layout (header/tabs) instead of keeping it mounted. Add a <Suspense> boundary around the org-detail outlet/content in the org-details page/view.
Lazy-loads the admin route pages so each ships as its own chunk rather than one eager bundle.
React.lazyper route + aSuspenseboundary in the layout (sidebar stays mounted while a page chunk loads).lazy(() => import(...))form.Further savings (follow-up)
There's a bigger scope that this PR doesn't tackle: deferring the ~245 KB gz
@raystack/frontier/adminview barrel off the initial load. We can't do that yet because the app shell (sidebar, config provider, route slugs) eagerly imports a few lightweight hooks from the same barrel that also holds all the views. That one eager import pins the whole barrel into the initial chunk, so thelazy()calls can't actually move the views out.To unlock it, we'd need a view-free SDK subpath that exposes just those config and hook exports, leaving the view barrel reachable only through dynamic imports.
We're planning to do exactly that while segregating the shared paths (the upcoming
/sharedrefactor), so the non-view exports move into their own entry and the view barrel can finally split into its own deferred chunk.