Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

`mode="static"` parses the whole document twice on mount (since the caret host in 2.7)

Aperta Adatta ai principianti
#673 0 commenti 1 reazione 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
72/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
react, typescript

Direzione di ricerca

Il doppio rendering deriva dallo stato hydrated in lib/use-caret-host.ts, che viene impostato in un effect dopo il mount e ri-renderizza Streamdown. La modalità static renderizza Markdown direttamente in index.tsx (il ramo mode === "static"), quindi il secondo render riesegue l'intero parsing. Inizia dal test di riproduzione nell'issue, che conta i render di un componente p personalizzato e dovrebbe passare quando il conteggio è 1. È completato quando quel test passa e la suite esistente continua a passare. La modifica con useSyncExternalStore suggerita nell'issue è un punto di partenza, ma verificala prima rispetto al markup di hydration.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug
Bug Description

In 2.7.0, every <Streamdown mode="static"> renders twice on mount, and the
second render parses the whole document again. Mounting a finished chat got
about 60% slower for us after upgrading from 2.4.0.

useCaretHost runs in every mode. It sets hydrated in an effect after mount:

// lib/use-caret-host.ts
const [hydrated, setHydrated] = useState(false);
useEffect(() => setHydrated(true), []);

That state update renders Streamdown again. Streaming mode survives this
because Block is memoized. Static mode renders <Markdown> directly
(index.tsx, the mode === "static" branch), so the second render runs the
full remark/rehype pipeline again. It also re-renders every custom component.

Impact on a real 36k-character chat (15 replies with KaTeX, lists and tables),
rendered in static mode in vitest + happy-dom:

2.4.0 2.7.0 2.7.0, no 2nd render
Mount, p50 65 ms 104 ms 70 ms
remark-parse time in a CPU profile 263 ms 521 ms —

We measured the last column with the smaller variant described at the end, which also removes the second render.

The second render runs after paint, so first paint is not delayed. It still
doubles the main-thread work for every static message that mounts.

Suggested fix: read hydrated from useSyncExternalStore. A client-only mount
gets true at once and does not render again. Hydration still renders with
false first, so the server markup keeps matching:

 import {
   type RefObject,
-  useEffect,
   useLayoutEffect,
   useRef,
-  useState,
+  useSyncExternalStore,
 } from "react";

 const HIDDEN_ATTRIBUTE = "data-sd-caret-hidden";

+const subscribeToNothing = () => () => {};
+
@@
-  const [hydrated, setHydrated] = useState(false);
-  useEffect(() => setHydrated(true), []);
+  const hydrated = useSyncExternalStore(
+    subscribeToNothing,
+    () => true,
+    () => false
+  );

With this change, the repro passes and the existing suite passes (93 files, 1242
tests).

A smaller change also works: set hydrated only when shouldHideCaret is true.
But then a caret that starts visible and later hides renders once with the caret
classes off. That causes the full-document style recalculation that the hook
tries to avoid.

Steps to Reproduce
import { render } from "@testing-library/react";
import { expect, it } from "vitest";
import { Streamdown } from "../index";

it("renders a static document once on mount", () => {
  let renders = 0;
  const P = ({ children }: { children?: React.ReactNode }) => {
    renders++;
    return <p>{children}</p>;
  };
  render(
    <Streamdown components={{ p: P }} mode="static">
      {"First paragraph.\n\nSecond paragraph."}
    </Streamdown>,
  );
  expect(renders).toBe(2);
});
Expected Behavior

It should only render once per paragraph, and not twice.

Actual Behavior

It renders the paragraphs twice.

Code Sample
import { render } from "@testing-library/react";
import { expect, it } from "vitest";
import { Streamdown } from "../index";

it("renders a static document once on mount", () => {
  let renders = 0;
  const P = ({ children }: { children?: React.ReactNode }) => {
    renders++;
    return <p>{children}</p>;
  };
  render(
    <Streamdown components={{ p: P }} mode="static">
      {"First paragraph.\n\nSecond paragraph."}
    </Streamdown>,
  );
  expect(renders).toBe(2);
});
Streamdown Version

2.7.0

React Version

19.3.0

Node.js Version

No response

Browser(s)

No response

Operating System

None

Additional Context

No response

Lingua principale
TypeScript
Stelle
5.7k
Fork
307
Merge medio
1g 23h
PR unite (30g)
21

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di vercel/streamdown

Tutte le issue di vercel/streamdown

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.