Skip to content

perf(editor): memoize the markdown preview render pipeline - #9843

Merged
nwparker merged 1 commit into
mainfrom
nwparker/perf-9-markdown-preview-memo
Jul 22, 2026
Merged

nwparker merged 1 commit into
mainfrom
nwparker/perf-9-markdown-preview-memo

Conversation

@nwparker

@nwparker nwparker commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

What

react-markdown's <Markdown> has no internal memoization: every render rebuilds the whole unified remark→rehype→highlight→katex processor and re-parses the document (react-markdown v10 lib/index.js runs createProcessor + parse + runSync per render). MarkdownPreview re-renders on internal state that doesn't affect the rendered output — most visibly, every keystroke in Find (query / match-index state) — so a large doc with code blocks + math re-ran the full parse + syntax-highlight + KaTeX pass on each keypress, making Find laggy.

This:

  • hoists the two fully-static plugin arrays to module scope (a fresh array identity per render would defeat the memo), and
  • renders the body through a React.memo'd MarkdownBody keyed on content + components.

The pipeline now re-runs only when the rendered content or the components map actually changes. The components map was already memoized, so its identity is stable across Find/body-unrelated re-renders. (Inline-annotation/review state deliberately rebuilds components and thus still re-runs the pipeline — required to keep the live annotation markup correct.)

ELI5

Every time you typed a letter in the preview's Find box, Orca re-cooked the entire markdown document from scratch — re-parsing it, re-syntax-highlighting every code block, re-rendering every math formula — just to draw a search highlight that doesn't change the document at all. On a big doc that made Find stutter. Now the cooked document is remembered and only re-cooked when the document actually changes, so Find (and other unrelated UI blips) are smooth.

Proof of zero regression

  • Pure memoization + hoisting static arrays; no output change. The components memo and its deps are untouched, so inline annotation cards / review-note highlights stay live (they intentionally rebuild components).
  • renderedContent is primitive string state; Object.is compares by value, so equal text safely skips and any real content change re-renders — no stale preview.
  • MarkdownBody adds no host DOM node and stays the same child under the translate="no" bodyRef container, so the OS-page-translation crash guard (fix(markdown-preview): paint Find matches without mutating react DOM (crash 237acef1) #8678) and the Find CSS-Custom-Highlight painting (which reads bodyRef) are unaffected — in fact the body DOM is now more stable during search.
  • 106 existing MarkdownPreview tests pass. Independent adversarial review (frontier model) additionally ran the full configured web typecheck (clean) and Vitest suite (33,840 tests pass) with the change present, and found no regression (only a comment-accuracy nit, now fixed).

Proof of perf improvement

The triage confirmed by reading node_modules/react-markdown/lib/index.js that the sync Markdown() has no memoization and rebuilds the processor + re-parses on every render. With the memoized body, Find keystrokes (and review-pulse/store re-renders that don't touch content/components) no longer trigger that O(document-size) parse + highlight + KaTeX pass — each avoided pass is the full pipeline over the whole document. The win scales with document size and the number of code/math blocks.

Made with Orca 🐋

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 62029dd1-a182-4e66-b9b9-9ed9ee34c46e

📥 Commits

Reviewing files that changed from the base of the PR and between a6d3e6a and d4d0d75.

📒 Files selected for processing (1)
  • src/renderer/src/components/editor/MarkdownPreview.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/components/editor/MarkdownPreview.tsx

📝 Walkthrough

Walkthrough

MarkdownPreview now uses a memoized MarkdownBody component to render markdown. Remark and rehype plugin arrays are hoisted to module scope and typed with ReactMarkdownOptions, keeping their identities stable across renders. The inline Markdown render was replaced with MarkdownBody, passing the rendered content and component mapping.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has strong summary/testing details, but it omits required Screenshots, AI Review Report, Security Audit, and Notes sections. Add the missing template sections, including screenshot status, AI review findings, a security audit summary, and any platform-specific notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: memoizing the markdown preview pipeline.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

react-markdown's <Markdown> has no internal memoization: it rebuilds the whole
unified remark->rehype->highlight->katex processor and re-parses the document on
every render. MarkdownPreview re-renders on internal state that does not affect
the rendered output — most visibly, every keystroke in Find (query/match-index
state) — so a large doc re-ran the full parse + syntax-highlight + KaTeX pass per
keypress, making Find laggy.

Hoist the two fully-static plugin arrays to module scope (a fresh array identity
per render would defeat the memo) and render the body through a React.memo'd
MarkdownBody keyed on content + components. The pipeline now re-runs only when the
rendered content or the components map actually changes; Find/review-pulse/copied-
note re-renders skip it. The components map was already memoized, so its identity
is stable across those re-renders.

Behavior unchanged: 106 existing MarkdownPreview tests pass.

Co-authored-by: Orca <help@stably.ai>
@nwparker
nwparker force-pushed the nwparker/perf-9-markdown-preview-memo branch from a6d3e6a to d4d0d75 Compare July 22, 2026 21:15
@nwparker
nwparker merged commit 17e471c into main Jul 22, 2026
1 check passed
@nwparker
nwparker deleted the nwparker/perf-9-markdown-preview-memo branch July 22, 2026 21:32
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…9843)

react-markdown's <Markdown> has no internal memoization: it rebuilds the whole
unified remark->rehype->highlight->katex processor and re-parses the document on
every render. MarkdownPreview re-renders on internal state that does not affect
the rendered output — most visibly, every keystroke in Find (query/match-index
state) — so a large doc re-ran the full parse + syntax-highlight + KaTeX pass per
keypress, making Find laggy.

Hoist the two fully-static plugin arrays to module scope (a fresh array identity
per render would defeat the memo) and render the body through a React.memo'd
MarkdownBody keyed on content + components. The pipeline now re-runs only when the
rendered content or the components map actually changes; Find/review-pulse/copied-
note re-renders skip it. The components map was already memoized, so its identity
is stable across those re-renders.

Behavior unchanged: 106 existing MarkdownPreview tests pass.

Co-authored-by: Orca <help@stably.ai>
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