feat(chat): lazy-load Mermaid markdown rendering#51
Conversation
Mingholy
left a comment
There was a problem hiding this comment.
Code Review: PR #51
Summary
This PR makes two related improvements to the markdown rendering pipeline:
-
Lazy-loads the
mermaidlibrary inMermaidBlock.tsxviaimport("mermaid")with module-scope caching, replacing the previous staticimport mermaid from "mermaid". This keeps the heavy mermaid renderer out of the initial bundle. -
Extracts per-language code block overrides (html, svg, xml, mermaid, plantuml, dot, graphviz) into a shared
createMarkdownLanguageComponents()factory in a newmarkdownLanguageComponents.tsxmodule, eliminating duplication betweenMarkdownBlock.tsxandassistant-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.
XmlHeaderin the shared module correctly forwards thenodeprop toCodeHeader, which the old inline version inMarkdownBlock.tsxomitted. This is a minor improvement.- No dead code left behind. The removed imports (
isSvgContent,SyntaxHighlighterProps,HtmlArtifactCard,FencedSvg,MermaidBlock,PlantUMLBlock,GraphvizBlock) fromMarkdownBlock.tsxare all accounted for in the new shared module. - The assertion script is added to
package.jsonastest:mermaid-lazy-loadbut 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>
|
已修复 rejected promise 缓存问题。 问题: 修复:在 修改文件: |
最终 Review 结论 ·
|
Summary
Test plan
cd web && bunx tsc -bbun run buildgit diff --checkweb, andserverpackage manifests do not define alintscript.Screenshots
No before/after screenshot is attached because this is a loading/bundle behavior change and Mermaid output is expected to remain visually equivalent.