Skip to content

perf(admin): lazy-load route pages#1666

Open
paanSinghCoder wants to merge 1 commit into
mainfrom
lazy-load-admin-routes
Open

perf(admin): lazy-load route pages#1666
paanSinghCoder wants to merge 1 commit into
mainfrom
lazy-load-admin-routes

Conversation

@paanSinghCoder
Copy link
Copy Markdown
Contributor

@paanSinghCoder paanSinghCoder commented Jun 2, 2026

Lazy-loads the admin route pages so each ships as its own chunk rather than one eager bundle.

  • React.lazy per route + a Suspense boundary in the layout (sidebar stays mounted while a page chunk loads).
  • Page components converted to default exports for the canonical lazy(() => import(...)) form.
  • ~105 KB gz off the initial bundle; the unauthenticated login screen no longer downloads page code.
  • App-only change — no SDK or public-API changes.

Further savings (follow-up)

There's a bigger scope that this PR doesn't tackle: deferring the ~245 KB gz @raystack/frontier/admin view 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 the lazy() 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 /shared refactor), so the non-view exports move into their own entry and the view barrel can finally split into its own deferred chunk.

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jun 2, 2026 8:56am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Implemented lazy loading for admin pages to improve performance and reduce initial bundle size.
    • Added loading indicators during route transitions for better user experience while pages load.

Walkthrough

This 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.

Changes

Code-splitting route pages

Layer / File(s) Summary
Suspense boundary for lazy loading in layout
web/apps/admin/src/App.tsx
App.tsx imports Suspense and LoadingState, then wraps the routed Outlet in a Suspense boundary with LoadingState as the fallback. A comment notes the boundary sits within the layout so the sidebar remains mounted during lazy route page loading.
Page component export conversions
web/apps/admin/src/pages/admins/AdminsPage.tsx, web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx, web/apps/admin/src/pages/invoices/InvoicesPage.tsx, web/apps/admin/src/pages/organizations/details/index.tsx, web/apps/admin/src/pages/organizations/list/index.tsx, web/apps/admin/src/pages/plans/PlansPage.tsx, web/apps/admin/src/pages/preferences/PreferencesPage.tsx, web/apps/admin/src/pages/products/ProductPricesPage.tsx, web/apps/admin/src/pages/products/ProductsPage.tsx, web/apps/admin/src/pages/roles/RolesPage.tsx, web/apps/admin/src/pages/users/UsersPage.tsx, web/apps/admin/src/pages/webhooks/WebhooksPage.tsx
All eleven page components change from named exports to default exports to enable React.lazy imports. Component logic and rendering remain unchanged.
Route configuration refactoring to lazy loading
web/apps/admin/src/routes.tsx
Top-level imports add lazy from React, eager imports for the app shell remain, and page components are replaced with React.lazy declarations. Organization nested views (security, members, projects, invoices, tokens, apis, pat) are also converted to lazy imports using import(...).then(...) patterns. The useAdminPaths hook remains a static eager import while other SDK components transition to lazy loading.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • raystack/frontier#1375: The AdminsPage export change and corresponding routes.tsx admin route wiring build on the prior PR's introduction of super-admins route infrastructure.

Suggested reviewers

  • rsbh
  • rohilsurana
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 26809372529

Coverage remained the same at 43.06%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37887
Covered Lines: 16314
Line Coverage: 43.06%
Coverage Strength: 12.08 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Wrap the nested <Outlet /> in a local Suspense boundary to avoid replacing the whole org details shell on lazy-tab load.

routes.tsx lazy-loads the org child views, and OrganizationDetailsView renders {children} directly (no Suspense boundary). Without a local boundary around the nested <Outlet />, the suspension bubbles to the top-level Suspense in App.tsx, which swaps out the entire routed content (including the org header/tabs) with LoadingState until 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e8b178 and bf6e07a.

📒 Files selected for processing (14)
  • web/apps/admin/src/App.tsx
  • web/apps/admin/src/pages/admins/AdminsPage.tsx
  • web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx
  • web/apps/admin/src/pages/invoices/InvoicesPage.tsx
  • web/apps/admin/src/pages/organizations/details/index.tsx
  • web/apps/admin/src/pages/organizations/list/index.tsx
  • web/apps/admin/src/pages/plans/PlansPage.tsx
  • web/apps/admin/src/pages/preferences/PreferencesPage.tsx
  • web/apps/admin/src/pages/products/ProductPricesPage.tsx
  • web/apps/admin/src/pages/products/ProductsPage.tsx
  • web/apps/admin/src/pages/roles/RolesPage.tsx
  • web/apps/admin/src/pages/users/UsersPage.tsx
  • web/apps/admin/src/pages/webhooks/WebhooksPage.tsx
  • web/apps/admin/src/routes.tsx

Comment on lines +38 to +72
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,
})),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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\\}" {} || true

Repository: 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.

@paanSinghCoder paanSinghCoder self-assigned this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants