From 6b3617586a057ea76755e6340fc7cfd7bae85d46 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:30:01 -0700 Subject: [PATCH] fix(editor): eliminate ~10s freeze when loading large markdown files (#870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rich-mode unsupported-content check ran canRoundTripRichMarkdown() unconditionally — synchronously creating a throwaway TipTap Editor, parsing the entire document, and serializing it back — all on the main thread during React render. For a 120KB file this blocked for ~10s. Redesign: run cheap regex checks first (reference links, footnotes, HTML). If no unsupported syntax is detected, allow rich mode immediately without any round-trip. The expensive round-trip is now only invoked when HTML is detected and the file is under 50K chars. --- .../components/editor/markdown-rich-mode.ts | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/components/editor/markdown-rich-mode.ts b/src/renderer/src/components/editor/markdown-rich-mode.ts index 915410b5..d35bd679 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode.ts @@ -1,4 +1,4 @@ -import { canRoundTripRichMarkdown, getRichMarkdownRoundTripOutput } from './markdown-round-trip' +import { getRichMarkdownRoundTripOutput } from './markdown-round-trip' import { extractFrontMatter } from './markdown-frontmatter' export type MarkdownRichModeUnsupportedReason = @@ -43,31 +43,35 @@ export function getMarkdownRichModeUnsupportedMessage(content: string): string | const contentWithoutCode = stripMarkdownCode(body) - if (canRoundTripRichMarkdown(body)) { - return null - } - - // Why: HTML/JSX gets special treatment — if the round-trip output preserves - // the embedded markup, we allow rich mode even though the pattern matched. - // Looked up by reason (not index) so reordering the array won't break this. + // Why: run cheap regex checks first. If no unsupported syntax is detected, + // rich mode is safe — no need for the expensive round-trip check. The + // round-trip (which synchronously creates a throwaway TipTap editor, parses + // the full document, and serializes it back) is only needed as a second + // opinion when HTML is detected, to verify the HTML survives the round-trip + // before blocking the user from rich mode. const htmlMatcher = UNSUPPORTED_PATTERNS.find((m) => m.reason === 'html-or-jsx') - if (htmlMatcher && htmlMatcher.pattern.test(contentWithoutCode)) { - const roundTripOutput = getRichMarkdownRoundTripOutput(body) - if (roundTripOutput && preservesEmbeddedHtml(contentWithoutCode, roundTripOutput)) { - return null - } - } + const hasHtml = htmlMatcher && htmlMatcher.pattern.test(contentWithoutCode) for (const matcher of UNSUPPORTED_PATTERNS) { + if (matcher.reason === 'html-or-jsx') { + continue + } if (matcher.pattern.test(contentWithoutCode)) { return matcher.message } } - // Why: Tiptap rewrites some harmless markdown spellings such as autolinks or - // escaped angle brackets even when the rendered document stays equivalent. - // Preview mode should stay editable unless we have a specific syntax we know - // the editor will drop or reinterpret in a user-visible way. + if (hasHtml) { + // Why: the round-trip check creates a throwaway TipTap Editor synchronously + // on the main thread. For large files this blocks for seconds, so we skip it and conservatively block rich mode for HTML files + // above this threshold. + const roundTripOutput = body.length <= 50_000 ? getRichMarkdownRoundTripOutput(body) : null + if (roundTripOutput && preservesEmbeddedHtml(contentWithoutCode, roundTripOutput)) { + return null + } + return htmlMatcher!.message + } + return null }