Skip to content

perf(markdown): reduce room-open render cost with parse cache and compiler memoization#7348

Merged
diegolmello merged 1 commit into
developfrom
cursor/5887f67d
Jun 2, 2026
Merged

perf(markdown): reduce room-open render cost with parse cache and compiler memoization#7348
diegolmello merged 1 commit into
developfrom
cursor/5887f67d

Conversation

@diegolmello
Copy link
Copy Markdown
Member

@diegolmello diegolmello commented May 27, 2026

Proposed changes

Opening a room was spending significant time re-parsing markdown for every message. This PR reduces that cost by:

  • Adding an LRU-style parse cache (max 200 entries) so identical message strings skip re-parsing
  • Extracting block rendering into a memoized MarkdownBlockView component with React Compiler 'use memo'
  • Stabilizing the markdown context value object to avoid unnecessary downstream re-renders
  • Moving inline block container styles into styles.ts

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1184

How to test or reproduce

  1. Open a room with many messages containing markdown (lists, code blocks, quotes, mentions, etc.)
  2. Compare room open time / scroll performance before and after
  3. Verify markdown still renders correctly for:
    • Plain text and formatted text
    • Lists, code blocks, quotes, headings
    • Mentions, channels, and custom emoji
    • Translated messages (isTranslated path should still re-parse)
  4. Navigate away and back to the same room — cached messages should render without visual regressions

Benchmark (iOS sim, Argent React Profiler)

Controlled A/B on the same app state — original Markdown vs this PR (compiler-only, no manual useMemo).

Control Value
Device iPhone 16e, open.rocket.chat, logged in
Flow Rooms list → tap Diego Mello → scroll → back
Gestures tap (0.5, 0.297), swipe (0.5,0.7)→(0.5,0.3), back (0.05, 0.07)
Cold start Metro reload before each run
Before Original Markdown (git stash) — session 20260527-160938
After Optimized Markdown (this PR) — session 20260527-163815
Metric Before After Δ
Primary mount batch (11×) 243.9ms self / 517ms commit 48.1ms self / 134ms commit −80% self / −74% commit
All Markdown mounts (44×, 7 commits) 295.8ms total self 69.8ms total self −76%
Incremental list batches (avg self/instance) ~2.5–3.5ms ~0.5–1.0ms ~3× faster

Per-commit Markdown self-time

Batch Before After
Primary (11×) 243.9ms 48.1ms
10.9ms 6.8ms
10.6ms 5.7ms
9.2ms 4.5ms
10.1ms 2.4ms
6.1ms 2.1ms
5.0ms 1.5ms

Caveats: Diego Mello DM is mostly voice-call system messages (InfoCard), not rich markdown — a text-heavy room would be a stronger benchmark. Dev-mode numbers; divide by ~3 for rough production estimates.

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

The parse cache uses a simple FIFO eviction when it exceeds 200 entries — enough to cover typical room message reuse without unbounded memory growth. Translated messages bypass the cache and always re-parse, preserving existing behavior for that path.

Summary by CodeRabbit

  • Refactor
    • Reworked markdown parsing and rendering into a modular block-based view for more reliable message display.
  • Performance
    • Added an in-memory parse cache to speed up rendering of repeated messages.
  • Bug Fixes
    • Parse errors are logged without breaking rendering; unknown/unsupported blocks are skipped gracefully.
  • Style
    • Adjusted block spacing for more consistent message layout.

@diegolmello diegolmello temporarily deployed to approve_e2e_testing May 27, 2026 17:08 — with GitHub Actions Inactive
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3eeb9093-13ba-4eb6-8a53-6536ff136425

📥 Commits

Reviewing files that changed from the base of the PR and between c14a1d9 and 8eb91db.

📒 Files selected for processing (2)
  • app/containers/markdown/index.tsx
  • app/containers/markdown/styles.ts
✅ Files skipped from review due to trivial changes (1)
  • app/containers/markdown/styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/containers/markdown/index.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test

Walkthrough

Refactors Markdown to parse messages via a bounded in-memory cache and resolveTokens helper, introduces MarkdownBlockView to render Root block types, and integrates these with a styled Markdown component that safely computes and renders tokens.

Changes

Markdown Container Rendering Refactor

Layer / File(s) Summary
Parser utilities and block renderer
app/containers/markdown/index.tsx
Introduces parseMessage with a 200-entry bounded cache, resolveTokens to reuse provided md or parse msg, and MarkdownBlockView which switches on Root block types and returns corresponding UI components (KaTeX, code, heading, etc.).
Component integration and styles
app/containers/markdown/styles.ts, app/containers/markdown/index.tsx
Adds blocks style (gap: 2) and imports it. Updates Markdown to compute tokens via resolveTokens inside try/catch (logs parse errors), return null when no tokens, build a contextValue, and render tokens.map with MarkdownBlockView inside the styled wrapper.
sequenceDiagram
  participant MarkdownComponent
  participant parseMessage
  participant MarkdownBlockView
  participant UIComponent
  MarkdownComponent->>parseMessage: resolveTokens(msg/md)
  parseMessage->>MarkdownBlockView: tokens (Root blocks)
  MarkdownBlockView->>UIComponent: render block (katex/code/heading/...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

type: chore

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main performance optimization: adding a parse cache and compiler memoization to reduce rendering costs when opening rooms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • NATIVE-1184: Request failed with status code 401

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.

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

🧹 Nitpick comments (1)
app/containers/markdown/index.tsx (1)

71-102: ⚡ Quick win

Consider adding exhaustive type checking to the switch statement.

The component correctly uses the React 19 Compiler 'use memo' directive and handles all current block types. However, the switch lacks compile-time exhaustiveness checking. If new block types are added to Root in the future, TypeScript won't alert you to update this component.

Optional: Add exhaustive check
 const MarkdownBlockView = ({ block }: { block: MarkdownBlock }) => {
 	'use memo';

 	switch (block.type) {
 		case 'BIG_EMOJI':
 			return <BigEmoji value={block.value} />;
 		case 'UNORDERED_LIST':
 			return <UnorderedList value={block.value} />;
 		case 'ORDERED_LIST':
 			return <OrderedList value={block.value} />;
 		case 'TASKS':
 			return <TaskList value={block.value} />;
 		case 'QUOTE':
 			return <Quote value={block.value} />;
 		case 'PARAGRAPH':
 			return <Paragraph value={block.value} />;
 		case 'CODE':
 			return <Code value={block.value} />;
 		case 'HEADING':
 			return <Heading value={block.value} level={block.level} />;
 		case 'LINE_BREAK':
 			return <LineBreak />;
 		case 'KATEX':
 			return <KaTeX value={block.value} />;
 		default:
+			// Exhaustive check: if a new block type is added, TypeScript will error here
+			const _exhaustive: never = block;
+			void _exhaustive;
 			return null;
 	}
 };

This ensures that if Root gains new block types, the compiler will flag this location.

🤖 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 `@app/containers/markdown/index.tsx` around lines 71 - 102, The switch in
MarkdownBlockView handling block.type should be made exhaustive so TypeScript
will error if a new MarkdownBlock type is added; add an assertUnreachable helper
(e.g., function assertUnreachable(x: never): never { throw new Error(`Unexpected
block type: ${String(x)}`); }) and replace the default branch with a call like
return assertUnreachable(block.type) (or assign block.type to a never-typed
variable and return it) so the compiler enforces exhaustiveness for
MarkdownBlockView and its block.type union.
🤖 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 `@app/containers/markdown/index.tsx`:
- Around line 63-69: The parameter type for resolveTokens is too narrow: change
the signature of resolveTokens (function name: resolveTokens) so msg accepts the
actual runtime types (e.g., msg: string | null | undefined) instead of just
string, and keep the existing defensive conversion inside (typeof msg ===
'string' ? msg : String(msg || '') ) so parseMessage receives a proper string;
ensure any callers that pass optional/null props still type-check against the
updated signature.

---

Nitpick comments:
In `@app/containers/markdown/index.tsx`:
- Around line 71-102: The switch in MarkdownBlockView handling block.type should
be made exhaustive so TypeScript will error if a new MarkdownBlock type is
added; add an assertUnreachable helper (e.g., function assertUnreachable(x:
never): never { throw new Error(`Unexpected block type: ${String(x)}`); }) and
replace the default branch with a call like return assertUnreachable(block.type)
(or assign block.type to a never-typed variable and return it) so the compiler
enforces exhaustiveness for MarkdownBlockView and its block.type union.
🪄 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: de187e0b-fe57-4ebf-ae5f-688914c0fb44

📥 Commits

Reviewing files that changed from the base of the PR and between 41444f4 and 8e5f82d.

📒 Files selected for processing (2)
  • app/containers/markdown/index.tsx
  • app/containers/markdown/styles.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/containers/markdown/styles.ts
  • app/containers/markdown/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode and baseUrl set to app/ for import resolution

Files:

  • app/containers/markdown/styles.ts
  • app/containers/markdown/index.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use @rocket.chat/eslint-config base with React, React Native, TypeScript, Jest plugins

Files:

  • app/containers/markdown/styles.ts
  • app/containers/markdown/index.tsx
app/containers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reusable UI components should be placed in app/containers/ directory

Files:

  • app/containers/markdown/styles.ts
  • app/containers/markdown/index.tsx
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/containers/markdown/styles.ts
  • app/containers/markdown/index.tsx
🔇 Additional comments (6)
app/containers/markdown/index.tsx (5)

21-21: LGTM!

Also applies to: 39-39


117-128: LGTM!


130-139: LGTM!


141-153: LGTM!


41-61: ⚡ Quick win

Confirm FIFO eviction logic; document/guard Root immutability for cached shared instances.

  • parseCache.keys().next().value relies on Map insertion order, so deletion is truly FIFO (and the 200-entry cap bounds memory).
  • Because parseCache is module-scoped, identical messages reuse the same Root instance; this is safe only if downstream code treats Root/its blocks as immutable (otherwise consider freezing/cloning or an explicit immutability guarantee).
app/containers/markdown/styles.ts (1)

11-13: LGTM!

Comment thread app/containers/markdown/index.tsx
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.73.0.108980

@diegolmello diegolmello temporarily deployed to approve_e2e_testing May 28, 2026 16:47 — with GitHub Actions Inactive
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

Copy link
Copy Markdown
Contributor

@OtavioStasiak OtavioStasiak left a comment

Choose a reason for hiding this comment

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

LGTM

…piler memoization

Markdown was a top offender when opening a room. Cache parsed tokens, split block rendering, stabilize context, and rely on React Compiler instead of manual useMemo.

Co-authored-by: Cursor <cursoragent@cursor.com>
@diegolmello diegolmello requested a deployment to approve_e2e_testing June 2, 2026 18:13 — with GitHub Actions Waiting
@diegolmello diegolmello merged commit 70d087b into develop Jun 2, 2026
5 of 8 checks passed
@diegolmello diegolmello deleted the cursor/5887f67d branch June 2, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants