Skip to content

Commit 75845c0

Browse files
authored
fix: Optimize code block rendering in streaming mode to prevent unnecessary re-renders (#436)
1 parent e50b0c4 commit 75845c0

File tree

3 files changed

+66
-10
lines changed

3 files changed

+66
-10
lines changed

.changeset/fix-code-block-memo.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"streamdown": patch
3+
---
4+
5+
Fix unnecessary re-renders of code blocks during streaming updates.
6+
7+
**Problem:** In streaming mode, when new content arrives (e.g. a paragraph is appended), completed code blocks that haven't changed were still re-rendering. This happened because the `Streamdown` component used inline object literals as default parameter values for `linkSafety` (`{ enabled: true }`). Every time `children` changed and `Streamdown` re-rendered, these inline defaults created new references, which caused the `contextValue` useMemo to recompute a new `StreamdownContext` object. Since React propagates context changes through `memo` boundaries, any context consumer inside a memoized `Block` (such as `CodeBlock`) would re-render even though the block's own props were unchanged.
8+
9+
**Fix:** Extract the inline default values for `linkSafety` into module-level constants (`defaultLinkSafetyConfig`). This ensures referential stability across renders, so `contextValue` only recomputes when the actual values change — not just because `children` updated.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { render, waitFor } from "@testing-library/react";
2+
import type { ReactNode } from "react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { Streamdown } from "../index";
5+
6+
let codeBlockRenderCount = 0;
7+
8+
vi.mock("../lib/code-block", () => ({
9+
CodeBlock: ({ code, children }: { code: string; children?: ReactNode }) => {
10+
codeBlockRenderCount += 1;
11+
return (
12+
<div data-testid="mock-code-block">
13+
<pre>{code}</pre>
14+
{children}
15+
</div>
16+
);
17+
},
18+
}));
19+
20+
describe("MemoCode in streaming mode", () => {
21+
it("should not re-render unchanged code blocks on streaming updates", async () => {
22+
codeBlockRenderCount = 0;
23+
24+
const initial = "```js\nconst a = 1;\n```\n\nParagraph one";
25+
const updated = "```js\nconst a = 1;\n```\n\nParagraph one plus";
26+
27+
const { container, rerender } = render(<Streamdown>{initial}</Streamdown>);
28+
29+
await waitFor(() => {
30+
expect(container.textContent).toContain("const a = 1;");
31+
expect(container.textContent).toContain("Paragraph one");
32+
});
33+
34+
const initialRenderCount = codeBlockRenderCount;
35+
expect(initialRenderCount).toBeGreaterThan(0);
36+
37+
rerender(<Streamdown>{updated}</Streamdown>);
38+
39+
await waitFor(() => {
40+
expect(container.textContent).toContain("Paragraph one plus");
41+
});
42+
43+
expect(codeBlockRenderCount).toBe(initialRenderCount);
44+
});
45+
});

packages/streamdown/index.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,22 @@ export interface StreamdownContextType {
284284
shikiTheme: [ThemeInput, ThemeInput];
285285
}
286286

287+
const defaultShikiTheme: [ThemeInput, ThemeInput] = [
288+
"github-light",
289+
"github-dark",
290+
];
291+
292+
const defaultLinkSafetyConfig: LinkSafetyConfig = {
293+
enabled: true,
294+
};
295+
287296
const defaultStreamdownContext: StreamdownContextType = {
288-
shikiTheme: ["github-light", "github-dark"],
297+
shikiTheme: defaultShikiTheme,
289298
controls: true,
290299
isAnimating: false,
291300
mode: "streaming",
292301
mermaid: undefined,
293-
linkSafety: { enabled: true },
302+
linkSafety: defaultLinkSafetyConfig,
294303
};
295304

296305
export const StreamdownContext = createContext<StreamdownContextType>(
@@ -413,11 +422,6 @@ export const Block = memo(
413422

414423
Block.displayName = "Block";
415424

416-
const defaultShikiTheme: [ThemeInput, ThemeInput] = [
417-
"github-light",
418-
"github-dark",
419-
];
420-
421425
export const Streamdown = memo(
422426
({
423427
children,
@@ -439,9 +443,7 @@ export const Streamdown = memo(
439443
caret,
440444
plugins,
441445
remend: remendOptions,
442-
linkSafety = {
443-
enabled: true,
444-
},
446+
linkSafety = defaultLinkSafetyConfig,
445447
allowedTags,
446448
literalTagContent,
447449
translations,

0 commit comments

Comments
 (0)