Skip to content

UN-3185: Defer heavy chunks on login route + cache fingerprinted assets#2114

Merged
jaseemjaskp merged 7 commits into
mainfrom
UN-3185-frontend-login-perf
Jun 26, 2026
Merged

UN-3185: Defer heavy chunks on login route + cache fingerprinted assets#2114
jaseemjaskp merged 7 commits into
mainfrom
UN-3185-frontend-login-perf

Conversation

@jaseemjaskp

Copy link
Copy Markdown
Contributor

What

  • Stop the unauthenticated /landing (login) page from eagerly downloading the full app, and cache fingerprinted assets so repeat visits don't re-fetch the bundle.
  • Fix 1 — frontend/nginx.conf: serve content-hashed /assets/* with Cache-Control: public, max-age=31536000, immutable; keep index.html and config/runtime-config.js no-cache.
  • Fix 2 — code-splitting: OSS route pages and enterprise plugin route elements → React.lazy behind a single <Suspense> in Router.jsx; new helper src/helpers/pluginRegistry.js (lazyPlugin) defers plugin chunks to navigation; app shell (PageLayout/FullPageLayout) made lazy.

Why

  • A DevTools trace of the cloud frontend showed /landing requesting 190+ chunks (PDF viewer, recharts, Monaco/ToolIde, every enterprise plugin) before the login screen paints, and content-hashed assets served with Cache-Control TTL 0 (~403 kB re-fetched every visit). Both hurt repeat visits and slow/mobile connections.

How

  • Static page imports → React.lazy(() => import(...)); one <Suspense fallback={<GenericLoader/>}> around <Routes>.
  • lazyPlugin(loader, exportName) wraps a plugin dynamic import as a lazy element; only referenced entrypoints enter the graph. In OSS the optionalPluginImports stub makes the import reject, and the element falls back to NotFound, so absent-plugin routes 404 harmlessly (same UX as before).
  • The two route-returning hooks (useVerticalsRoutes, useLlmWhispererRoutes) and PRODUCT_NAMES stay guarded await imports (consumed synchronously). Their heavy page/shell imports are lazy-loaded in the cloud plugin repo (companion PR).
  • PageLayout/FullPageLayout made lazy — they statically pulled SideNavBarlookup-studio → PDF/charts onto /landing.

Can this PR break any existing features?

  • Low risk. Route paths, auth guards (RequireAuth/RequireGuest/RequireAdmin), and behavior are unchanged. Lazy routes now show a brief GenericLoader on first load of each chunk. OSS-parity build (with src/plugins removed) verified green: missing plugins resolve to the stub and routes fall through to NotFound exactly as before.

Database Migrations

  • None.

Env Config

  • None.

Relevant Docs

  • None.

Related Issues or PRs

  • UN-3185. Companion PR in unstract-cloud lazy-loads the verticals / llm-whisperer route hooks' pages.

Dependencies Versions

  • None (uses React lazy/Suspense, already present).

Notes on Testing

  • npm run build green (with and without src/plugins). Biome clean.
  • Measured /landing script requests via Chrome DevTools: 201 → 93. pdf-vendor (548K), recharts CategoricalChart (284K), Monaco/ToolIde, and verticals/llm-whisperer pages no longer load until navigation.
  • Recommend a manual click-through of authenticated routes (dashboard, tools/:id, settings) to confirm the lazy shell renders.

Screenshots

Checklist

I have read and understood the Contribution Guidelines.

Fix 1 (nginx.conf): serve content-hashed /assets/* with a 1y immutable
Cache-Control while keeping index.html and config/runtime-config.js
non-cached, so repeat visits stop re-fetching the whole bundle.

Fix 2: stop the unauthenticated /landing page from eagerly downloading
the full app. Convert OSS route pages and enterprise plugin route
elements to React.lazy behind a single <Suspense>, and lazy-load the
app shell (PageLayout/FullPageLayout) which transitively pulled the
PDF viewer, charts and lookup-studio onto /landing.

New helper src/helpers/pluginRegistry.js (lazyPlugin) defers plugin
chunks to navigation; OSS builds resolve the stub and fall back to
NotFound, so absent-plugin routes 404 harmlessly as before.

Measured /landing script requests: 201 -> 93; pdf-vendor, recharts and
Monaco no longer load until navigated to. OSS-parity build (no
src/plugins) verified.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The frontend now lazy-loads selected routes and plugin-backed pages through shared helpers and suspense/error boundaries, while nginx maps cache headers by URI and serves hashed assets with long-lived caching.

Changes

Frontend lazy loading and cache policy

Layer / File(s) Summary
Cache policy and asset serving
frontend/nginx.conf
The nginx server maps request URIs to cache-control values, applies the header at server scope, and restricts /assets/ to existing files served by GET and HEAD only.
Lazy module helpers
frontend/src/helpers/lazyNamed.js, frontend/src/helpers/pluginRegistry.js
lazyNamed adapts named exports for React.lazy, and lazyPlugin resolves plugin modules while turning the optional-plugin stub error into NotFound.
Route fallbacks and outlet recovery
frontend/src/components/widgets/error-boundary/ErrorBoundary.jsx, frontend/src/components/error/LazyOutlet/LazyOutlet.jsx, frontend/src/layouts/fullpage-payout/FullPageLayout.jsx, frontend/src/layouts/page-layout/PageLayout.jsx
ErrorBoundary gains reset-key recovery, LazyOutlet adds suspense and route-scoped error handling, and the page layouts render LazyOutlet instead of Outlet.
Lazy route registration
frontend/src/routes/useMainAppRoutes.js, frontend/src/routes/Router.jsx
Route and plugin entry points switch to lazy loading, guarded plugin route trees keep their module checks, and the router is wrapped in suspense and error boundaries while preserving the authenticated route structure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two main changes: lazy-loading heavy login-route chunks and caching fingerprinted assets.
Description check ✅ Passed The description follows the repository template and fills the required sections with clear details about what, why, how, risks, testing, and related work.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-3185-frontend-login-perf

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.

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements code-splitting for the React frontend to prevent the unauthenticated /landing route from eagerly downloading the full application bundle, and adds long-term immutable caching for Vite's content-hashed assets via nginx. The nginx fix uses a map directive at http scope to drive Cache-Control from a single server-level add_header, cleanly avoiding the header-inheritance problem that would arise from per-location add_header calls.

  • nginx: a map $uri $cache_control selects immutable for /assets/* and no-cache everywhere else; a dedicated /assets/ location block returns hard 404 (not the SPA fallback) for missing chunks.
  • Code-splitting: all route pages and enterprise plugins are wrapped in React.lazy (via new helpers lazyNamed and lazyPlugin); layouts PageLayout/FullPageLayout replace <Outlet> with the new <LazyOutlet>, which scopes its own <Suspense> + <ErrorBoundary> to the content area; a top-level <ErrorBoundary> in Router.jsx covers the shell and routes outside layouts, with auto-reload-once logic for stale-chunk failures.
  • OSS parity: plugins absent from OSS builds resolve via the Vite stub to NotFound; the lazyPlugin helper maps only the "Optional plugin not available" sentinel to NotFound and re-throws all other errors to the boundary.

Confidence Score: 5/5

Safe to merge. Route paths, auth guards, and OSS/cloud parity are all preserved; the two previously-identified issues (security-header inheritance in nginx, missing ErrorBoundary around Suspense) were resolved in earlier commits and are correctly addressed in this version.

The nginx change is correct — the map + server-level add_header approach composes cleanly with existing security headers. The lazy-loading strategy is well-scoped: LandingPage stays eager, layouts and plugin pages are deferred, and the two hook-returning route plugins that must run synchronously are explicitly documented as remaining as guarded awaits. Error recovery through LazyOutlet's scoped boundary and the app-wide backstop in Router.jsx is sound. The only substantive observation is a silent fallback-to-default-export edge case in lazyPlugin that affects only misconfigured cloud plugins and not the OSS path.

No files require special attention. frontend/src/helpers/pluginRegistry.js has a minor edge case with the named-export fallback, but it only matters when a cloud plugin renames an export and has an unrelated default export simultaneously.

Important Files Changed

Filename Overview
frontend/nginx.conf Adds map $uri $cache_control at http scope and a server-level add_header Cache-Control so immutable caching for /assets/* composes cleanly with existing security headers instead of overriding them; dedicated /assets/ location block returns hard 404 for missing files.
frontend/src/helpers/pluginRegistry.js New lazyPlugin helper wraps enterprise plugin imports as React.lazy components; correctly maps only the 'Optional plugin not available' stub error to NotFound and re-throws all others, but m[exportName] ?? m.default silently falls back to the default export when a named export is absent.
frontend/src/components/error/LazyOutlet/LazyOutlet.jsx New component that scopes Suspense+ErrorBoundary to the content area; auto-reload-once logic for stale-chunk failures uses a 10-second sessionStorage guard to prevent reload loops; navigation resets the boundary via resetKeys.
frontend/src/routes/Router.jsx Wraps routes in a top-level ErrorBoundary+Suspense; converts eagerly-imported pages and plugin components to lazy equivalents; guarded await imports for hook-returning route plugins remain with improved error logging.
frontend/src/routes/useMainAppRoutes.js Replaces all eager imports with lazyNamed/lazyPlugin; layouts are now lazy-loaded so the SideNavBar/lookup-studio/PDF graph is no longer pulled onto /landing; PRODUCT_NAMES guard refined to check the specific unstract key.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Browser navigates to /landing"] --> B["nginx serves index.html\n(Cache-Control: no-cache)"]
    B --> C["React app boots\nEager: LandingPage only"]
    C --> D{"Authenticated?"}
    D -- No --> E["Render LandingPage\nNo heavy chunks fetched"]
    D -- Yes --> F["Navigate to authenticated route"]
    F --> G["Outer Suspense+ErrorBoundary\nin Router.jsx"]
    G --> H{"Layout type?"}
    H -- PageLayout / FullPageLayout --> I["lazyNamed loads layout chunk\n/assets/PageLayout-hash.js\n(Cache-Control: immutable)"]
    I --> J["Layout renders LazyOutlet\n(inner Suspense+ErrorBoundary)"]
    J --> K["lazyNamed / lazyPlugin\nloads page chunk on navigation"]
    K --> L["Page renders in content area\nShell stays mounted"]
    H -- No layout route --> M["lazyPlugin loads plugin chunk"]
    M --> N{"Plugin absent in OSS?"}
    N -- Yes --> O["Stub throws sentinel\nlazyPlugin returns NotFound"]
    N -- No --> P["Component renders normally"]
    K -- "Chunk load error" --> Q["Inner ErrorBoundary catches\nhandleRouteError: auto-reload once\n(sessionStorage guard)"]
    Q -- "Reloaded" --> R["Fresh index.html\nNew content-hashed chunk URLs"]
    Q -- "Within 10s window" --> S["Show RouteLoadError\n'Reload' button"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Browser navigates to /landing"] --> B["nginx serves index.html\n(Cache-Control: no-cache)"]
    B --> C["React app boots\nEager: LandingPage only"]
    C --> D{"Authenticated?"}
    D -- No --> E["Render LandingPage\nNo heavy chunks fetched"]
    D -- Yes --> F["Navigate to authenticated route"]
    F --> G["Outer Suspense+ErrorBoundary\nin Router.jsx"]
    G --> H{"Layout type?"}
    H -- PageLayout / FullPageLayout --> I["lazyNamed loads layout chunk\n/assets/PageLayout-hash.js\n(Cache-Control: immutable)"]
    I --> J["Layout renders LazyOutlet\n(inner Suspense+ErrorBoundary)"]
    J --> K["lazyNamed / lazyPlugin\nloads page chunk on navigation"]
    K --> L["Page renders in content area\nShell stays mounted"]
    H -- No layout route --> M["lazyPlugin loads plugin chunk"]
    M --> N{"Plugin absent in OSS?"}
    N -- Yes --> O["Stub throws sentinel\nlazyPlugin returns NotFound"]
    N -- No --> P["Component renders normally"]
    K -- "Chunk load error" --> Q["Inner ErrorBoundary catches\nhandleRouteError: auto-reload once\n(sessionStorage guard)"]
    Q -- "Reloaded" --> R["Fresh index.html\nNew content-hashed chunk URLs"]
    Q -- "Within 10s window" --> S["Show RouteLoadError\n'Reload' button"]
Loading

Reviews (6): Last reviewed commit: "UN-3185: Auto-reload once on chunk-load ..." | Re-trigger Greptile

Comment thread frontend/nginx.conf Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/routes/useMainAppRoutes.js (1)

351-356: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate the product wrapper on the value actually used.

Object.keys(PRODUCT_NAMES).length can be true even when PRODUCT_NAMES.unstract is absent, but Line 355 passes that value as type. Use the unstract entry itself as the condition to avoid wrapping all app routes with an undefined product type.

Proposed fix
-  if (OnboardProduct && Object.keys(PRODUCT_NAMES)?.length) {
+  const unstractProduct = PRODUCT_NAMES?.unstract;
+
+  if (OnboardProduct && unstractProduct) {
     return (
       <Route
         path=""
-        element={<OnboardProduct type={PRODUCT_NAMES?.unstract} />}
+        element={<OnboardProduct type={unstractProduct} />}
       >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/useMainAppRoutes.js` around lines 351 - 356, The product
wrapper guard in useMainAppRoutes is checking Object.keys(PRODUCT_NAMES).length,
but the route element actually depends on PRODUCT_NAMES.unstract; update the
condition in the OnboardProduct/Route block to gate on the same unstract value
that is passed as type, so the wrapper is only rendered when that product type
exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/nginx.conf`:
- Around line 61-68: The location blocks that use add_header are overriding the
server-level security headers, so explicitly re-add the security headers in each
affected location stanza. Update the nginx.conf location handlers for /assets/,
/index.html, and /config/runtime-config.js so they preserve the same
X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and
Content-Security-Policy-Report-Only values defined at the server level, while
keeping the existing cache-related headers in place.

In `@frontend/src/helpers/pluginRegistry.js`:
- Around line 26-28: Update the lazy import error handling in pluginRegistry.js
so chunk fetch failures are not treated as missing plugins. In lazyPlugin, the
catch path currently relies on isModuleMissing(err) and returns NotFound for
both the build-time stub and transient dynamic import/network failures; tighten
isModuleMissing to only match the optional-plugin stub error or change the
lazyPlugin catch logic to only map that specific case to NotFound and rethrow
all other errors.

In `@frontend/src/routes/Router.jsx`:
- Around line 113-127: The top-level dynamic imports in Router.jsx are still
running during module evaluation, which keeps useLlmWhispererRoutes and
useVerticalsRoutes on the startup critical path. Move these imports out of the
Router module scope and into lazy-loaded route/component wrappers so the plugins
load only when their routes are actually needed. Use the existing symbols
llmWhispererRouter, verticalsRouter, useLlmWhispererRoutes, and
useVerticalsRoutes to relocate the loading logic without blocking /landing
rendering.

---

Outside diff comments:
In `@frontend/src/routes/useMainAppRoutes.js`:
- Around line 351-356: The product wrapper guard in useMainAppRoutes is checking
Object.keys(PRODUCT_NAMES).length, but the route element actually depends on
PRODUCT_NAMES.unstract; update the condition in the OnboardProduct/Route block
to gate on the same unstract value that is passed as type, so the wrapper is
only rendered when that product type exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92f99d45-c009-4324-8547-15d11fbbaa70

📥 Commits

Reviewing files that changed from the base of the PR and between 069a177 and 425feac.

📒 Files selected for processing (4)
  • frontend/nginx.conf
  • frontend/src/helpers/pluginRegistry.js
  • frontend/src/routes/Router.jsx
  • frontend/src/routes/useMainAppRoutes.js

Comment thread frontend/nginx.conf
Comment thread frontend/src/helpers/pluginRegistry.js
Comment thread frontend/src/routes/Router.jsx
…bsent check

- nginx: drive Cache-Control from a $uri map at server scope instead of
  per-location add_header. Location-level add_header replaces (not merges)
  the inherited server headers, which dropped X-Content-Type-Options/
  X-Frame-Options/Referrer-Policy/CSP from /assets, index.html and
  runtime-config.js. Now both locations carry no add_header and inherit all
  security + cache headers.
- pluginRegistry: only treat the build-time stub ('Optional plugin not
  available') as plugin-absent; rethrow transient chunk-load failures of a
  shipped plugin instead of masking them as NotFound.
- useMainAppRoutes: gate the OnboardProduct wrapper on PRODUCT_NAMES.unstract
  (the value passed as type) rather than the map being non-empty.
@jaseemjaskp

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews 🙏 Addressed in 020ef555b:

# Finding Action
1 nginx: location-level add_header drops inherited security headers (greptile P1 + CodeRabbit) Fixed — Cache-Control now driven by a map $uri $cache_control at server scope; both location blocks carry no add_header, so they inherit all security + cache headers.
2 pluginRegistry masks transient chunk-fetch failures as missing (CodeRabbit) FixedlazyPlugin matches only the build-stub 'Optional plugin not available' and rethrows all other errors.
3 Top-level await for the two route hooks still on /landing path (CodeRabbit) Won't fix (documented) — the hooks return a <Route> tree consumed synchronously; they can't be React.lazy without a route-config rewrite. Companion cloud PR Zipstack/unstract-cloud#1605 already split their pages/shell out, so each chunk is ~8 KB now (was 744 KB / 46 KB).
4 Gate OnboardProduct on PRODUCT_NAMES.unstract, not map length (CodeRabbit, outside diff) Fixed — now gates on unstractProduct = PRODUCT_NAMES?.unstract, the same value passed as type. More relevant now since OnboardProduct is always-truthy via lazyPlugin.

Build green, biome clean. One caveat: I couldn't run nginx -t locally (no Docker daemon up) — the map/inheritance pattern is standard nginx, but please confirm in the image build/CI.

Comment thread frontend/src/routes/Router.jsx Outdated
Pull the repeated 'lazy a named export' pattern into src/helpers/lazyNamed.js
and use it in Router.jsx and useMainAppRoutes.js instead of inline
.then((m) => ({ default: m.X })) / a per-file 'named' helper. Behaviour is
identical; /landing chunk count unchanged. Addresses cloud-PR review feedback
about the duplicated helper (the two route hooks import the same util).

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/helpers/lazyNamed.js`:
- Around line 11-12: The lazyNamed helper currently maps any requested export to
{ default: m[exportName] }, which can silently return undefined and fail later
in React.lazy. Update lazyNamed(loader, exportName) to explicitly validate that
the loaded module contains exportName before resolving, and throw a descriptive
error during the loader promise chain when it is missing. Keep the fix localized
to lazyNamed and ensure the error makes it clear which missing named export
caused the load failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 70d9876e-a614-4c86-9aa0-e5dbd86bb5de

📥 Commits

Reviewing files that changed from the base of the PR and between 020ef55 and 9864b3f.

📒 Files selected for processing (3)
  • frontend/src/helpers/lazyNamed.js
  • frontend/src/routes/Router.jsx
  • frontend/src/routes/useMainAppRoutes.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/routes/useMainAppRoutes.js
  • frontend/src/routes/Router.jsx

Comment thread frontend/src/helpers/lazyNamed.js Outdated
- Wrap the route <Suspense> in Router.jsx with the existing ErrorBoundary and
  a reload-prompt fallback. lazyPlugin/lazyNamed rethrow non-stub failures
  (e.g. a transient chunk-load blip), and App rendered <Router/> with no
  boundary — so such a failure would unmount the tree to a blank screen.
  The boundary now contains it and offers a reload (which re-fetches the chunk).
- lazyNamed: throw a descriptive error when the requested named export is
  missing instead of handing React.lazy { default: undefined } (opaque error).

Addresses greptile P1 (no ErrorBoundary) and CodeRabbit (lazyNamed validation).

@jaseemjaskp jaseemjaskp left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated follow-up review (PR Review Toolkit). I raised the bar to only correctness/contract/resilience issues that weren't already covered in earlier rounds. Three findings below; the rest (comment-accuracy nit on the FullPageLayout rationale, nginx no-cache wording, missing helper unit tests, a behavior-preserving dedup of the guarded-await import blocks) are minor and left out of inline comments.

Comment thread frontend/src/helpers/pluginRegistry.js Outdated
Comment thread frontend/src/routes/Router.jsx Outdated
Comment thread frontend/src/routes/useMainAppRoutes.js
… dead guard

Review round 3:
- lazyPlugin: validate the resolved export (mirror lazyNamed). A shipped plugin
  whose named export was renamed/removed now throws a descriptive error instead
  of handing React.lazy { default: undefined }; isPluginAbsent doesn't match it,
  so it re-throws to the ErrorBoundary rather than masking as NotFound.
- ErrorBoundary: support resetKeys — clear the error when a key changes (e.g.
  location), so navigation recovers without a full reload.
- New LazyOutlet (content-scoped <Suspense> + nav-resettable ErrorBoundary +
  <Outlet/>); PageLayout/FullPageLayout use it instead of a bare <Outlet/>.
  Per-page load spinners and chunk-load failures now stay in the content area
  with the shell mounted; the app-wide boundary in Router.jsx remains the
  backstop and is now also location-reset. Fixes the blast-radius / no-recovery
  and shell-blanks-on-first-nav issues from the single top-level boundary.
- useMainAppRoutes: remove the now-dead 'ReadOnlyReviewPage && !ReviewLayout'
  warning — with lazyPlugin both are always truthy so it could never fire; the
  route degrades to NotFound if manual-review is absent.

Build green (with and without src/plugins); biome clean.
…ud S7764)

Prefer globalThis over window for the reload handler, matching existing
globalThis.location usage in the codebase (e.g. SideNavBar). Clears the only
open SonarCloud issue on this PR (javascript:S7764, minor code smell).
…loy)

The route ErrorBoundary already catches a rejected dynamic import() (a
<Suspense> alone only handles the pending state). Add chunk-error handling so
the common production trigger — a stale hashed chunk after a redeploy (client
requests a filename the CDN no longer serves) — auto-recovers:

- isChunkLoadError() detects the failed-dynamic-import error across browsers.
- handleRouteError() (wired as onError on both the app-wide boundary in
  Router.jsx and the content-scoped one in LazyOutlet) reloads ONCE to pick up
  fresh chunk hashes, guarded by a sessionStorage timestamp so a genuinely-gone
  chunk falls through to the manual Reload fallback instead of looping.
  Non-chunk render errors are never auto-reloaded.

This covers the outermost lazy route elements too (e.g. FullPageLayout for
verticals, OnboardProduct for llm-whisperer), which render under the Router
boundary.
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@jaseemjaskp jaseemjaskp merged commit b13e5bd into main Jun 26, 2026
9 checks passed
@jaseemjaskp jaseemjaskp deleted the UN-3185-frontend-login-perf branch June 26, 2026 04:55
@sonarqubecloud

Copy link
Copy Markdown

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