Skip to content

feat(chat): lazy-load Mermaid markdown rendering#51

Open
Mingholy wants to merge 2 commits into
mainfrom
feat/ft-5-mermaid-lazy-load
Open

feat(chat): lazy-load Mermaid markdown rendering#51
Mingholy wants to merge 2 commits into
mainfrom
feat/ft-5-mermaid-lazy-load

Conversation

@Mingholy

@Mingholy Mingholy commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Lazy-load Mermaid rendering so the heavy renderer is not part of the initial markdown path.
  • Keep markdown language component behavior shared through a factory helper.
  • Add a source-level assertion to protect against accidentally eager-loading Mermaid again.

Test plan

  • cd web && bunx tsc -b
  • bun run build
  • git diff --check
  • Mermaid lazy-load source assertion was run during implementation.
  • Lint not run: root, web, and server package manifests do not define a lint script.

Screenshots

No before/after screenshot is attached because this is a loading/bundle behavior change and Mermaid output is expected to remain visually equivalent.

@Mingholy Mingholy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Code Review: PR #51

Summary

This PR makes two related improvements to the markdown rendering pipeline:

  1. Lazy-loads the mermaid library in MermaidBlock.tsx via import("mermaid") with module-scope caching, replacing the previous static import mermaid from "mermaid". This keeps the heavy mermaid renderer out of the initial bundle.

  2. Extracts per-language code block overrides (html, svg, xml, mermaid, plantuml, dot, graphviz) into a shared createMarkdownLanguageComponents() factory in a new markdownLanguageComponents.tsx module, eliminating duplication between MarkdownBlock.tsx and assistant-ui/markdown-text.tsx.

A source-level assertion script (assert-mermaid-lazy-load.mjs) is added to guard against regression.

Implementation Quality

Lazy-load pattern -- good. The loadMermaid() singleton pattern (cache the promise at module scope, resolve to the instance) is the standard approach and matches what GraphvizBlock.tsx already does for @viz-js/viz. The mermaidInstance fast-path avoids an extra microtask on subsequent calls.

Factory extraction -- good. createMarkdownLanguageComponents(CodeHeader) cleanly parameterizes over the one piece that differs between the two call sites (each has its own CodeHeader with different styling). The resulting code in both consumers is a single one-liner.

Assertion script -- useful but limited. The regex-based source inspection is a pragmatic way to catch accidental reverts. Worth noting: the componentsByLanguage={markdownLanguageComponents} assertion is whitespace-sensitive. If a formatter inserts a space around =, it would false-fail.

One concern -- cached rejection in loadMermaid():

let mermaidPromise: Promise<MermaidInstance> | null = null;

function loadMermaid(): Promise<MermaidInstance> {
  if (mermaidInstance) return Promise.resolve(mermaidInstance);
  if (!mermaidPromise) {
    mermaidPromise = import("mermaid").then((module) => {
      mermaidInstance = module.default;
      return mermaidInstance;
    });
  }
  return mermaidPromise;
}

If the dynamic import rejects (network error, script load failure), mermaidPromise is set to a rejected promise and never cleared. Every subsequent mermaid block in the session will hit the same cached rejection with no way to recover short of a page reload.

A simple fix would be to clear the promise on rejection:

mermaidPromise = import("mermaid").then(
  (module) => {
    mermaidInstance = module.default;
    return mermaidInstance;
  },
  (err) => {
    mermaidPromise = null;   // allow retry
    throw err;
  },
);

This is a pre-existing pattern in GraphvizBlock.tsx as well (same cached-rejection risk there), so it is not a blocking issue for this PR specifically, but worth fixing here since you are already touching this code.

Code Quality

  • Clean diff. The extraction is mechanical and the before/after behavior is identical. Imports are properly updated in both consumers.
  • XmlHeader in the shared module correctly forwards the node prop to CodeHeader, which the old inline version in MarkdownBlock.tsx omitted. This is a minor improvement.
  • No dead code left behind. The removed imports (isSvgContent, SyntaxHighlighterProps, HtmlArtifactCard, FencedSvg, MermaidBlock, PlantUMLBlock, GraphvizBlock) from MarkdownBlock.tsx are all accounted for in the new shared module.
  • The assertion script is added to package.json as test:mermaid-lazy-load but is not wired into any CI step. Consider adding it to a CI check or pre-commit hook if you want it to actually prevent regressions.

Security

No security concerns. The change is purely a bundle-splitting optimization and a code organization refactor. The securityLevel: "strict" setting on mermaid is preserved, and SVG sanitization is untouched.

Verdict

LGTM with one suggestion. The lazy-load and shared factory extraction are clean and correct. The one substantive suggestion is handling the rejected-promise cache in loadMermaid() so that a transient import failure does not permanently disable mermaid rendering for the session. This is non-blocking since GraphvizBlock has the same pattern today, but it would be a nice improvement to fix here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mingholy

Mingholy commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

已修复 rejected promise 缓存问题。

问题loadMermaid() 在动态 import 失败时,mermaidPromise 会被设为一个 rejected promise 且永久缓存,导致用户会话内后续所有 mermaid block 都命中这个失败状态,无法重试。

修复:在 import("mermaid") 的 rejection handler 中将 mermaidPromise 重置为 null,使下次调用可以重新发起 import,实现自动重试。

修改文件:web/src/components/chat/MermaidBlock.tsx,commit 5945a0d

@Mingholy

Mingholy commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

最终 Review 结论 · ⚠️ 可合并(有 nit)

意图与达成度:完全达成。mermaid 由静态 import 改为模块级缓存的动态 import("mermaid"),将重型渲染器移出首屏 bundle;两处重复的 per-language code block 配置抽取为共享工厂 createMarkdownLanguageComponents()。无行为回归。

历史 review comment:唯一阻塞性意见(loadMermaid() 缓存 rejected promise → 一次 import 失败会永久禁用本会话 mermaid 渲染)已在 5945a0d 修复 —— rejection handler 中重置 mermaidPromise = null,下次可自动重试。✅

安全:无新增风险。securityLevel: "strict" 保留,SVG 经 sanitizeSvg 挂载到隔离 shadow root,XSS 防护路径未改动;未引入新依赖。

遗留 nit(非阻塞)

  • web/scripts/assert-mermaid-lazy-load.mjs:43,47 正则空白敏感,formatter 在 = 周围插空格会误报;建议改 \s*=\s*
  • package.json:37test:mermaid-lazy-load 未接入 CI,防回归能力有限,可合并后跟进。
  • GraphvizBlock.tsx:15-22 存在同样的 cached-rejection 模式(pre-existing,不在本 PR 范围,可另开 PR)。

无 Critical/High/Medium 问题。

🤖 Generated with Claude Code

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