perf(web): revisiting a thread reuses its rendered Markdown instead of parsing again#27480

merged1
main
t3/optimize-markdown-rendering
opened20h agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 6h ago

Every time a reply mounts, react-markdown parses it again: when you switch threads, when a row scrolls back into view, or when a turn fold opens. On long threads this made Markdown the second-largest render cost when switching threads, after ChatView.

This PR swaps the <ReactMarkdown> component for a small renderer in chat/markdownRenderer.ts. It keeps the same engine (unified, micromark and our remark/rehype plugins) and produces the same output. The differences:

  • One frozen processor per plugin set. Previously a new one was built for every message.
  • Finished replies are cached as React elements, up to about 10 MB (500K source characters). Least recently used entries are dropped first. A reply that would take more than a quarter of the cache isn't cached, so a single huge message can't push the others out.
  • Streaming text and the incremental-parsing plugin skip the cache and render as before.

Measured on a production build with real threads (389 and 1,114 replies) copied from a live database:

Markdown parse timeBeforeAfter
10 switches between two large threads151–166 ms64 ms (only first visits parse; revisits 0)
Scrolling a long thread22 ms3 ms
Streaming a reply~290 msunchanged

A first visit to a thread still parses everything; only a faster engine would help that.

A new test renders a set of cases with both the new renderer and <ReactMarkdown> and checks the markup matches. The cases cover raw HTML, <details>, scripts, tables, task lists, alerts, code meta, footnotes and URL transforms. Another test checks that rendering one oversized message keeps the rest of the cache.

Opus 5.5 via Claude Code

🤖 Generated with Claude Code

Checks

26 successful · 13 skipped
All checks passed
9db5777

Added 1 commit

  • perf(web): revisiting a thread reuses its rendered Markdown instead of parsing again9db5777
Synced from GitHub

Approvability

Verdict: Approved at 9db5777

Macroscope's review found this PR approvable — Completed Markdown rendering is reused through a bounded in-memory cache, while streaming and instance-specific plugins avoid cache insertion. Existing sanitization and URL handling remain in place, with no persistent-data or product-default changes.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB−6 B (−0.1%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.3 KiB1.2 KiB−6 B (−0.5%)2.0 KiB✅
CodexLive turn WebSocket decoded21.6 KiB21.6 KiB0 B (0.0%)24.4 KiB✅
CodexLive turn messages220 (0.0%)8✅
ClaudeTotal thread wire5.0 KiB5.1 KiB+6 B (+0.1%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.3 KiB1.3 KiB+6 B (+0.5%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
ClaudeLive turn messages220 (0.0%)8✅

Baseline: 32ecd71 · PR result: 9db5777 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

External image Files ignored due to path filters: 1
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 9db57770d4e8b48f98835bac31be4fa5af0a0b27 and 9db57770d4e8b48f98835bac31be4fa5af0a0b27.

External image Files ignored due to path filters: 1
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 32ecd71726fee16f335484c1f95cbce126d461f6 and 9db57770d4e8b48f98835bac31be4fa5af0a0b27.

External image Files ignored due to path filters: 1
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
External image Files selected for processing: 4
  • apps/web/package.json
  • apps/web/src/components/ChatMarkdown.tsx
  • apps/web/src/components/chat/markdownRenderer.test.tsx
  • apps/web/src/components/chat/markdownRenderer.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Walkthrough

Chat Markdown now renders through a configurable renderer instead of ReactMarkdown. The renderer applies URL transforms and caches rendered elements. Chat rendering selects shared or per-instance plugin lists and enables caching for completed, non-streaming text.

Changes: Chat Markdown rendering

Layer / File(s)Summary
Renderer, cache, and validation
apps/web/package.json, apps/web/src/components/chat/markdownRenderer.ts, apps/web/src/components/chat/markdownRenderer.test.tsx
Adds renderer dependencies and createMarkdownRenderer. The renderer processes Markdown, transforms applicable URL attributes, and caches rendered elements within an estimated-size limit. Tests compare output with ReactMarkdown and check cache reuse.
Chat rendering integration
apps/web/src/components/ChatMarkdown.tsx
Moves URL handling to module scope and routes Markdown through the new renderer. It reuses renderers for shared plugin lists and passes caching flags for completed text.

Priority: External image Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Sequence Diagram(s)

mermaid
1sequenceDiagram
2 participant ChatMarkdown
3 participant MarkdownSource
4 participant createMarkdownRenderer
5 ChatMarkdown->>MarkdownSource: Pass text, plugin settings, and sharing flags
6 MarkdownSource->>createMarkdownRenderer: Create a renderer when plugins are not shared
7 createMarkdownRenderer-->>MarkdownSource: Return a renderer
8 MarkdownSource->>createMarkdownRenderer: Render text with the cache setting
9 createMarkdownRenderer-->>MarkdownSource: Return a React element

Suggested reviewers: maria-rcks

Merge Risk: External image Minimal · up to 9db57

No actionable issue is established; the change is mergeable subject to normal checks.

Pre-merge checks: Passed 3 · Failed 1

Failed Failed checks: 1 warning

Check nameStatusExplanationResolution
Description checkWarningThe description follows the template's main sections in substance. The Problem section is covered by the opening paragraph, which explains that Markdown reparses on every mount and was the second-larg…Add a Scope and approval section. Link the triaged bug issue or the maintainer-approved discussion, including the approval comment. If no prior issue exists, explain why this change qualifies as a very small, focused fix of an obvious bug. …
Passed Passed checks: 3 passed
Check nameStatusExplanation
Title checkPassedThe title names the main change: revisiting a thread reuses cached rendered Markdown instead of parsing again. The changeset adds a markdown renderer with a per-message cache of rendered React element…
Linked Issues checkPassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes checkPassedCheck skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: revisiting a thread reuses cached rendered Markdown instead of parsing again. The changeset adds a markdown renderer with a per-message cache of rendered React elements, which matches the title. The title uses the conventional commit prefix perf(web): and is specific enough for a teammate scanning history. It is 84 characters, which is longer than ideal but still readable.

Full details: Description check

Explanation

The description follows the template's main sections in substance. The Problem section is covered by the opening paragraph, which explains that Markdown reparses on every mount and was the second-largest render cost when switching threads. The Change section explains the renderer swap, the frozen processors, the cache limits, and the streaming bypass. Verification includes measured timings from a production build, and it lists the new parity and cache-eviction tests. The Scope and approval section is missing. It does not link a triaged bug issue or maintainer discussion, and it gives no justification for skipping one. The description also lacks the required before/after evidence framing for UI-adjacent behavior, although the change is primarily performance-related and the measurement table partly covers this. The AI-attribution line is present.

Resolution

Add a Scope and approval section. Link the triaged bug issue or the maintainer-approved discussion, including the approval comment. If no prior issue exists, explain why this change qualifies as a very small, focused fix of an obvious bug. Also confirm that the Verification section states what was not checked, such as first-visit behavior in other clients or browsers, if applicable.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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