Skip to content

ADR-013: Route-based deferred loading for dynamic frontend plugins - #23

Closed
its-mitesh-kumar wants to merge 2 commits into
mainfrom
adr-013-route-based-deferred-loading
Closed

ADR-013: Route-based deferred loading for dynamic frontend plugins#23
its-mitesh-kumar wants to merge 2 commits into
mainfrom
adr-013-route-based-deferred-loading

Conversation

@its-mitesh-kumar

@its-mitesh-kumar its-mitesh-kumar commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Documents the RHDH-specific approach for route-based deferred loading of dynamic frontend plugins
  • Reduces initial page load from 12s TTI (40+ plugins loaded eagerly) to under 3s by deferring plugins until route navigation
  • Aligns with upstream RFC backstage/backstage#35037 but provides an independent fallback via rhdhDynamicFeaturesLoader

Performance Report by LightHouse

Lightspeed.pdf

Key decisions

  1. Convention-based: Most plugins route at /<pluginId> — inferred automatically, zero changes needed
  2. Metadata for exceptions: Only ~5-8 plugins need explicit backstage.routes in package.json
  3. Operator escape hatches: app.extensions for non-conventional paths, dynamicPlugins.loading.eager for app-wide plugins
  4. Entity-content plugins: Already use ExtensionBoundary.lazy() internally, no action needed

Test plan

  • Review ADR content for accuracy and completeness
  • Verify plugin classification tables are correct
  • Confirm escape hatch mechanisms align with existing NFS config schema

Documents the approach for deferring dynamic plugin loading until route
navigation, reducing initial load from 12s TTI to under 3s with 40+ plugins.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Add ADR-013 for route-based deferred loading of dynamic frontend plugins

📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Document RHDH route-based deferred loading strategy for dynamic frontend plugins.
• Specify convention/metadata/config priority for eager vs deferred plugin loading.
• Capture alternatives and consequences, aligned with upstream Backstage RFC.
Diagram

graph TD
  U["User navigates to route"] --> L["Dynamic plugin loader"] --> D{"Classify plugin"}
  D -->|"Eager"| E["Load at startup"] --> R["Fetch/execute remote bundles"]
  D -->|"Deferred"| W["Wait for matching path"] --> R
  C1["Config: dynamicPlugins.loading.eager"] --> D
  C2["Config: app.extensions path override"] --> D
  M["Metadata: backstage.routes"] --> D
  V["Convention: /<pluginId>"] --> D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep this as a design doc in README/wiki instead of an ADR
  • ➕ Lower process overhead; easier to iterate quickly
  • ➕ Can be more free-form and include operational notes
  • ➖ Harder to track decisions over time and enforce consistency
  • ➖ Less discoverable/standardized than an ADR index
2. Split into two ADRs (routing metadata vs loader behavior)
  • ➕ Clear separation of concerns (metadata contract vs runtime strategy)
  • ➕ Easier to supersede/deprecate only one part when upstream lands
  • ➖ More documents to maintain; may fragment a single coherent narrative
  • ➖ More up-front effort for reviewers and authors

Recommendation: Keep the ADR as a single, end-to-end decision record: it ties performance motivation, the convention/metadata strategy, operator escape hatches, and upstream alignment into one reviewable unit. Consider splitting only if implementation work later diverges (e.g., metadata contract ships independently of the loader).

Files changed (1) +208 / -0

Documentation (1) +208 / -0
013-route-based-deferred-loading.mdAdd ADR-013 documenting route-based deferred loading strategy +208/-0

Add ADR-013 documenting route-based deferred loading strategy

• Introduces an Architecture Decision Record describing why eager dynamic plugin loading is a performance bottleneck and how route-based deferral addresses it. Defines a convention-first routing inference model, explicit 'backstage.routes' metadata for exceptions, and operator escape hatches via existing 'app.extensions' and a proposed 'dynamicPlugins.loading.eager' list.

decisions/013-route-based-deferred-loading.md

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

Great, no actions required

You can view lower severity findings below.

View more (3)
Remediation recommended
1. Invalid JS example 🐞 Bug ⚙ Maintainability
Description
The ADR’s loader snippet is not valid JavaScript: it assigns to an undeclared module, references
an undefined remoteModuleName, and the inner map does not return a value. This can mislead
readers into copying an example that would throw at runtime or produce incorrect results.
Code

decisions/013-route-based-deferred-loading.md[R25-28]

+    const moduleFeatures = await Promise.all(
+      remote.exposedModules.map(async (exposedModuleName) => {
+        module = await instance.loadRemote(remoteModuleName);
+      })
Relevance

●●● Strong

Team has accepted fixes for invalid/misleading examples in ADRs; this is a clear correctness/doc
fix.

PR-#2

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The snippet uses module = ... without a declaration and calls
instance.loadRemote(remoteModuleName) even though only exposedModuleName is in scope; the
callback also lacks a return, so Promise.all(...) would collect undefined values.

decisions/013-route-based-deferred-loading.md[22-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The JavaScript snippet intended to illustrate the current eager-loading behavior contains undefined identifiers and missing returns, so it is invalid as written.

### Issue Context
Even in ADRs, code snippets are often copied into implementation discussions; invalid examples reduce trust and can cause wasted time.

### Fix Focus Areas
- decisions/013-route-based-deferred-loading.md[20-34]

### Suggested fix
- Either (a) explicitly label it as pseudocode, OR (b) make it valid JS by:
 - declaring variables (`const module = ...`),
 - using the correct identifier (`exposedModuleName` instead of `remoteModuleName`, or show how `remoteModuleName` is derived),
 - returning the loaded value from the `map` callback (e.g., `return instance.loadRemote(...)`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Ambiguous routes schema 🐞 Bug ≡ Correctness
Description
The ADR documents backstage.routes as a per-extension map for page plugins but then uses a
different object shape ({"path":"*"}) for app-wide plugins, leaving the metadata schema ambiguous.
This ambiguity can lead different implementers to encode/parse backstage.routes differently,
undermining the ADR’s goal of “metadata-only” classification.
Code

decisions/013-route-based-deferred-loading.md[R65-68]

+{
+  "backstage": {
+    "routes": { "path": "*" }
+  }
Relevance

●●● Strong

They’ve accepted ADR edits to make schemas/examples consistent across sections; this matches that
precedent.

PR-#2

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The page-plugin example shows routes as a map keyed by page:techdocs, but the app-wide example
changes routes to an object with a direct path property, which is a different structure and is
not explained as a supported union.

decisions/013-route-based-deferred-loading.md[52-60]
decisions/013-route-based-deferred-loading.md[63-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The ADR presents two different JSON shapes for `backstage.routes` (map keyed by extension id vs a direct `{ "path": "*" }` object). This makes the metadata schema unclear.

### Issue Context
The ADR relies on `backstage.routes` being served/consumed as lightweight metadata, so its structure must be unambiguous.

### Fix Focus Areas
- decisions/013-route-based-deferred-loading.md[50-70]

### Suggested fix
- Pick a single consistent schema for `backstage.routes` and update both examples to match.
- If you intend to support multiple shapes (union types), explicitly document the union and the parsing semantics (how the loader/backend distinguishes app-wide vs per-extension entries), and update examples accordingly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Informational
3. RFC link in code 🐞 Bug ⚙ Maintainability
Description
The upstream RFC URL is wrapped in inline-code formatting, which typically prevents Markdown from
rendering it as a clickable link. This adds friction for readers trying to open the referenced
issue.
Code

decisions/013-route-based-deferred-loading.md[36]

+An upstream RFC has been filed at `https://github.com/backstage/backstage/issues/35037`. This ADR documents the RHDH-specific fallback approach if the upstream RFC is delayed or not accepted.
Relevance

●●● Strong

Minor Markdown usability nit; likely accepted as low-risk documentation improvement (they’ve
accepted adding/clarifying links).

PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The URL is explicitly placed inside backticks in the ADR, making it render as inline code rather
than a standard link in many Markdown renderers.

decisions/013-route-based-deferred-loading.md[36-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The ADR wraps the RFC URL in backticks, which commonly disables auto-linking.

### Issue Context
This is a documentation usability issue; the ADR references the RFC as supporting context.

### Fix Focus Areas
- decisions/013-route-based-deferred-loading.md[36-36]

### Suggested fix
- Replace the inline code span with a normal Markdown link, e.g. `[backstage/backstage#35037](https://github.com/backstage/backstage/issues/35037)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Replace incorrect metrics from local Backstage dev build (which did not
use dynamic plugin loading) with real RHDH cluster Lighthouse data:
55/100 score, 13.9s LCP with 16-18 dynamic plugins.

Co-authored-by: Cursor <cursoragent@cursor.com>
@its-mitesh-kumar

Copy link
Copy Markdown
Member Author

I verified on the cluster that dynamic import() is working properly — UI components (React pages, entity tabs) are loaded lazily on demand via PageBlueprint.loader. My ADR-23 was also working in that direction, and since the mechanism is already functional, I am closing my ADR.

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.

1 participant