diff --git a/.changeset/mermaid-stable-svg-html.md b/.changeset/mermaid-stable-svg-html.md new file mode 100644 index 00000000..bd1a62b0 --- /dev/null +++ b/.changeset/mermaid-stable-svg-html.md @@ -0,0 +1,5 @@ +--- +"streamdown": patch +--- + +perf: stop re-parsing a Mermaid diagram's SVG every time it re-renders with the same chart diff --git a/packages/streamdown/__tests__/mermaid-component.test.tsx b/packages/streamdown/__tests__/mermaid-component.test.tsx index d8f90953..c6de6e8b 100644 --- a/packages/streamdown/__tests__/mermaid-component.test.tsx +++ b/packages/streamdown/__tests__/mermaid-component.test.tsx @@ -247,6 +247,21 @@ describe("Mermaid Component", () => { }); }); + it("should keep the rendered SVG node when re-rendering the same chart", async () => { + const { container, rerender } = renderWithContext( + + ); + + await waitFor(() => { + expect(container.querySelector('[role="img"] svg')).toBeTruthy(); + }); + const svg = container.querySelector('[role="img"] svg'); + + rerender(); + + expect(container.querySelector('[role="img"] svg')).toBe(svg); + }); + it("should generate unique IDs for multiple charts", async () => { const mockRender = vi.fn().mockResolvedValue({ svg: "Chart", diff --git a/packages/streamdown/lib/mermaid/index.tsx b/packages/streamdown/lib/mermaid/index.tsx index d86d350f..6fc5d5a7 100644 --- a/packages/streamdown/lib/mermaid/index.tsx +++ b/packages/streamdown/lib/mermaid/index.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useDeferredRender } from "../../hooks/use-deferred-render"; import { StreamdownContext } from "../../index"; import { useIsCodeFenceIncomplete } from "../block-incomplete-context"; @@ -210,6 +210,11 @@ export const Mermaid = ({ renderLatest(); }, [chart, config, retryCount, shouldRender, mermaidPlugin]); + // Always render the SVG if we have content (either current or last valid) + const displaySvg = svgContent || lastValidSvg; + // react-dom 19.2 rewrites innerHTML whenever this object's identity changes + const svgHtml = useMemo(() => ({ __html: displaySvg }), [displaySvg]); + // Show placeholder when not scheduled to render if (!(shouldRender || svgContent || lastValidSvg)) { return ( @@ -275,9 +280,6 @@ export const Mermaid = ({ ); } - // Always render the SVG if we have content (either current or last valid) - const displaySvg = svgContent || lastValidSvg; - return (