Skip to content

perf(ui): lazy load activity feed editor surfaces#29178

Open
shah-harshit wants to merge 2 commits into
feat/dashboard-lcp-parentfrom
feat/dashboard-lcp-02-activity-feed
Open

perf(ui): lazy load activity feed editor surfaces#29178
shah-harshit wants to merge 2 commits into
feat/dashboard-lcp-parentfrom
feat/dashboard-lcp-02-activity-feed

Conversation

@shah-harshit

@shah-harshit shah-harshit commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Moves activity feed/editor-heavy surfaces behind lazy boundaries and keeps feed cache behavior scoped to activity feed usage.
  • Split from the dashboard LCP optimization work so it can be reviewed independently.

Testing

  • Not run; tests will be fixed separately for this PR.

Ref: https://github.com/open-metadata/openmetadata-collate/issues/4230


Summary by Gitar

  • Performance optimizations:
    • Lazy-loaded RichTextEditorPreviewerV1, Reactions, and ActivityFeedEditor in FeedCardBody.
    • Implemented myActivityFeedCache using an LRU cache and request coalescing in ActivityFeedProvider to prevent redundant API calls.
  • Utility refactoring:
    • Extracted core markdown-to-html conversion and content formatting logic into BlockEditorPureUtils to reduce bundle dependencies.
    • Replaced formatContent with formatClientContent across multiple previewer components.
  • Code structure:
    • Migrated task-related navigation utilities to TaskNavigationUtils and action utilities to TaskActionUtils for better modularity.
    • Added clearActivityFeedCache to handle session-based cleanup of cached feed data.

This will update automatically on new commits.

Greptile Summary

This PR moves activity-feed and editor-heavy surfaces behind React.lazy + withSuspenseFallback boundaries to reduce the initial bundle size and improve LCP on the dashboard. It also extracts the client-side markdown-to-HTML formatting logic from BlockEditorUtils into a new BlockEditorPureUtils file to eliminate heavy editor dependencies from previewer components.

  • Converts static imports of RichTextEditorPreviewerV1, ActivityFeedEditor, Reactions, and a dozen feed sub-components to lazy-loaded variants across five files.
  • Introduces formatClientContent in BlockEditorPureUtils.ts as a pure extraction of the formatContent(_, 'client') path, and updates three previewer components and their tests to use it.
  • Updates TaskDescriptionPreviewer tests to use findBy/waitFor to accommodate the newly async lazy BlockEditor.

Confidence Score: 5/5

Safe to merge — all changes are mechanical lazy-load conversions and a clean utility extraction with no behavioural regressions.

The lazy-loading conversions are straightforward pattern applications of an already-established withSuspenseFallback HOC. The formatClientContent extraction is a direct copy of the 'client' branch of formatContent, with a minor improvement (longer FQN matches are now prioritised to prevent substring collisions). No data flow, state management, or API logic is altered.

ActivityPanelBody.tsx is worth a second look: it lazy-loads ProfilePicture and UserPopOverCard, which are small inline components that briefly show a full-layout spinner on first load.

Important Files Changed

Filename Overview
openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorPureUtils.ts Adds formatClientContent (extracted from formatContent(_, 'client') in BlockEditorUtils) and isHTMLString — functionally equivalent to the originals but now duplicates isHTMLString in a second file with a subtly different error-handling path.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/FeedCardBody/FeedCardBody.tsx Converts static imports of RichTextEditorPreviewerV1, Reactions, and ActivityFeedEditor to lazy imports via withSuspenseFallback; no logic changes.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/ActivityPanelBody.tsx Lazy-loads ProfilePicture and UserPopOverCard — small, lightweight inline components — alongside the heavier editor components, using a full-layout-area Loader as fallback, which may cause layout shifts on first render.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody.tsx Migrates several direct imports (ErrorPlaceHolder, ConfirmationModal, FeedPanelHeader, TaskFeedCardFromTask, ActivityThread, ActivityThreadList, ActivityFeedEditor) to lazy-loaded variants; no logic changes.
openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/TaskDescriptionPreviewer.tsx Lazy-loads BlockEditor and replaces formatContent(markdown, 'client') with formatClientContent(markdown); tests updated to use waitFor/findBy for async rendering.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Before["Before (static imports)"]
        A[FeedCardBody] -->|static| B[RichTextEditorPreviewerV1]
        A -->|static| C[Reactions]
        A -->|static| D[ActivityFeedEditor]
        E[ActivityPanelBody] -->|static| F[ProfilePicture]
        E -->|static| G[UserPopOverCard]
        E -->|static| H[ActivityFeedEditorNew]
        I[ActivityThreadPanelBody] -->|static| J[ActivityThread]
        I -->|static| K[ActivityThreadList]
        I -->|static| L[ConfirmationModal]
    end

    subgraph After["After (lazy imports)"]
        A2[FeedCardBody] -->|lazy + Suspense| B2[RichTextEditorPreviewerV1]
        A2 -->|lazy + Suspense| C2[Reactions]
        A2 -->|lazy + Suspense| D2[ActivityFeedEditor]
        E2[ActivityPanelBody] -->|lazy + Suspense| F2[ProfilePicture]
        E2 -->|lazy + Suspense| G2[UserPopOverCard]
        E2 -->|lazy + Suspense| H2[ActivityFeedEditorNew]
        I2[ActivityThreadPanelBody] -->|lazy + Suspense| J2[ActivityThread]
        I2 -->|lazy + Suspense| K2[ActivityThreadList]
        I2 -->|lazy + Suspense| L2[ConfirmationModal]
    end

    subgraph Utils["Utility Refactor"]
        M[BlockEditorUtils.ts] -->|extracted client path| N[BlockEditorPureUtils.ts]
        N --> O[formatClientContent]
        N --> P[isHTMLString duplicate]
        O --> Q[RichTextEditorPreviewerV1]
        O --> R[RichTextEditorPreviewerNew]
        O --> S[TaskDescriptionPreviewer]
    end
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
    subgraph Before["Before (static imports)"]
        A[FeedCardBody] -->|static| B[RichTextEditorPreviewerV1]
        A -->|static| C[Reactions]
        A -->|static| D[ActivityFeedEditor]
        E[ActivityPanelBody] -->|static| F[ProfilePicture]
        E -->|static| G[UserPopOverCard]
        E -->|static| H[ActivityFeedEditorNew]
        I[ActivityThreadPanelBody] -->|static| J[ActivityThread]
        I -->|static| K[ActivityThreadList]
        I -->|static| L[ConfirmationModal]
    end

    subgraph After["After (lazy imports)"]
        A2[FeedCardBody] -->|lazy + Suspense| B2[RichTextEditorPreviewerV1]
        A2 -->|lazy + Suspense| C2[Reactions]
        A2 -->|lazy + Suspense| D2[ActivityFeedEditor]
        E2[ActivityPanelBody] -->|lazy + Suspense| F2[ProfilePicture]
        E2 -->|lazy + Suspense| G2[UserPopOverCard]
        E2 -->|lazy + Suspense| H2[ActivityFeedEditorNew]
        I2[ActivityThreadPanelBody] -->|lazy + Suspense| J2[ActivityThread]
        I2 -->|lazy + Suspense| K2[ActivityThreadList]
        I2 -->|lazy + Suspense| L2[ConfirmationModal]
    end

    subgraph Utils["Utility Refactor"]
        M[BlockEditorUtils.ts] -->|extracted client path| N[BlockEditorPureUtils.ts]
        N --> O[formatClientContent]
        N --> P[isHTMLString duplicate]
        O --> Q[RichTextEditorPreviewerV1]
        O --> R[RichTextEditorPreviewerNew]
        O --> S[TaskDescriptionPreviewer]
    end
Loading

Reviews (2): Last reviewed commit: "fix(ui): remove activity feed cache spli..." | Re-trigger Greptile

@shah-harshit shah-harshit requested a review from a team as a code owner June 18, 2026 10:32
@shah-harshit shah-harshit added UI UI specific issues safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check labels Jun 18, 2026
@shah-harshit shah-harshit self-assigned this Jun 18, 2026
Comment thread openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorPureUtils.ts Outdated
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

❌ UI Checkstyle Failed

❌ ESLint + Prettier + Organise Imports (src)

One or more source files have linting or formatting issues.

Affected files
  • openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/FeedCardBody/FeedCardBodyNew.tsx
    • openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody.tsx

Fix locally (fast — only checks files changed in this branch):

make ui-checkstyle-changed

@shah-harshit shah-harshit changed the base branch from main to feat/dashboard-lcp-parent June 18, 2026 10:59
@gitar-bot

gitar-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 resolved / 4 findings

Implements lazy loading for activity feed editors and adds LRU caching to reduce bundle size and API overhead. Resolves compilation errors, cache invalidation bugs, and logout cleanup issues identified during the review.

✅ 4 resolved
Bug: ActivityFeedCache imports non-existent modules (build break)

📄 openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedCache.ts:15-16 📄 openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedCache.ts:20-22
ActivityFeedCache.ts imports MAX_ACTIVITY_FEED_CACHE_ENTRIES from ../../../constants/DashboardCache.constants and makeLruCache from ../../../utils/lruCacheUtils. Neither module exists in this branch — Glob/Grep across openmetadata-ui/.../ui/src find no DashboardCache.constants.*, no lruCacheUtils.*, and makeLruCache is referenced only in this new file. The PR description notes this was "split from the dashboard LCP optimization work", so these dependencies presumably live in the unmerged sibling PR. As-is the UI build (tsc/webpack) will fail to resolve these imports, breaking compilation of the entire ActivityFeedProvider import chain. Either include the cache utility/constant in this PR or vendor a bounded LRU here.

Security: Stale/cross-user feed applied to state after cache invalidation

📄 openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.tsx:800-814
In fetchMyActivityFeedHandler, the captured epoch only guards the cache write (if (epoch === activityFeedCacheEpoch) myActivityFeedCache.set(...)). The subsequent setActivityEvents(data) runs unconditionally after await request. If clearActivityFeedCache() is called (logout / user switch / domain change) while a request is in flight, the resolved promise still pushes the now-stale data into component state, so a different user's activity feed can be rendered after the switch. Guard the state update with the epoch as well, e.g. only call setActivityEvents(data) when epoch === activityFeedCacheEpoch.

Security: clearActivityFeedCache never invoked on logout/user switch

📄 openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedCache.ts:18-32
myActivityFeedCache, myActivityFeedRequests, and activityFeedCacheEpoch are module-level singletons that persist for the lifetime of the JS bundle. The file comment states the cache is kept outside the provider "so auth/logout can clear it", and clearActivityFeedCache() is exported for that purpose, but it is only ever called from ActivityFeedProvider.test.tsx — no production logout/auth/user-switch path invokes it (verified in AuthProvider logout handler). As a result, one user's cached activity feed can be served to the next user who logs in within the same browser session (privacy/data-leak) and stale data survives user switches. Wire clearActivityFeedCache() into the logout / user-change flow.

Bug: replaceAll with string replacement mis-handles '$' in FQN/href

📄 openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorPureUtils.ts:44-55
In convertMarkdownFormatToHtmlString, the generated entityLink is passed as the replacement string to String.prototype.replaceAll(key, entityLink). When the replacement is a string, $ sequences (e.g. $&, $$, $1) are interpreted specially. FQNs/hrefs containing $ are valid and would corrupt the inserted anchor markup. Additionally replaceAll(key, ...) does a plain substring replace, so a mention/hashtag token that is a substring of another could be replaced incorrectly. Consider escaping the replacement (or using a replacer function) to insert the link literally.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant