{"slug": "show-hn-hypermarkdown-streaming-markdown-renderer-for-react", "title": "Show HN: HyperMarkdown, streaming Markdown renderer for React", "summary": "Æven released HyperMarkdown, a streaming-native Markdown renderer for React that caches settled content down to code lines, table rows, and list items, achieving 1.6×–10.6× faster rendering than the nearest streaming renderer in benchmarks. On a large streaming table, HyperMarkdown completed the workload in under one second, compared to 33 seconds for Streamdown, 56 seconds for DeepSeek Harness, and 30 seconds for react-markdown. The renderer is available as @aeven-ai/hypermarkdown on npm and supports React 18 and 19.", "body_md": "**Parse the change. Not the conversation.**\n\nHyperMarkdown is a streaming-native Markdown renderer built for LLM output. It caches settled content down to code lines, table rows, and list items, so growing responses do not keep paying to parse and render work that is already finished.\n\n**1.6×–10.6× faster than the nearest streaming renderer across our benchmark\nsuite.**\n\n| Workload | HyperMarkdown | Markstream | Streamdown | DeepSeek Harness | react-markdown |\n|---|---|---|---|---|---|\n| Large code block | 216 ms |\n769 ms | 3,242 ms | 4,416 ms | 2,054 ms |\n| Mixed prose | 153 ms |\n313 ms | 559 ms | 249 ms | 2,629 ms |\nCaptured AI code stream (`real-code-os` ) |\n659 ms |\n4,417 ms | 12,511 ms | 14,621 ms | 8,008 ms |\nCaptured AI table stream (`real-table-head` ) |\n654 ms |\n4,948 ms | 11,621 ms | 19,644 ms | 10,236 ms |\n| Large table | 874 ms |\n9,276 ms | 33,142 ms | 55,918 ms | 29,747 ms |\n\nThe captured model fixtures are not generated stress cases: their content is\nreal AI output, replayed in controlled 8-character frames. HyperMarkdown\nrenders the code stream in **659 ms** versus **4,417 ms** for the next closest\nstreaming renderer, and the table stream in **654 ms** versus **4,948 ms**.\n\nOn a large streaming table, HyperMarkdown completes the workload in **under\none second**.\n\n- Streamdown:\n**33 seconds** - DeepSeek Harness strategy:\n**56 seconds** - react-markdown:\n**30 seconds**\n\nSame Markdown. Same stream. Very different architecture.\n\nProduction React benchmark on an Apple M2 Max, including chunk processing and synchronous render/commit. Absolute timings vary; the ratios are the useful comparison.\n\n[Read the methodology](/Aeven-AI/HyperMarkdown/blob/main/benchmarks/README.md) ·\n[View the full benchmark results](/Aeven-AI/HyperMarkdown/blob/main/benchmarks/results/latest.md)\n\nMost streaming Markdown renderers optimize at the document or block level. HyperMarkdown goes further.\n\n```\nTraditional streaming renderer\n\nnew token\n   ↓\ngrowing active block\n   ↓\nparse the active block again\n   ↓\nrender again\n```\n\nHyperMarkdown:\n\n```\nnew token\n   ↓\nactive block\n   │\n   ├── settled code lines    → cached\n   ├── settled table rows    → cached\n   ├── settled list items    → cached\n   └── changing frontier     → parse\n```\n\nA 1,000-line code block does not become a 1,000-line parsing problem every time another token arrives.\n\n**Completed work stays completed.**\n\n- Sub-block caching for code, tables, and lists\n- Streaming-safe handling of incomplete Markdown\n- GFM tables, task lists, autolinks, and footnotes\n- Reasoning blocks written as\n`<think>`\n\n,`<thinking>`\n\n, or`<reasoning>`\n\n- KaTeX math\n- Mermaid diagrams\n- Syntax highlighting\n- Raw HTML with sanitization\n- React 18 and React 19\n- SSR and hydration, including a Next.js App Router client boundary\n- Lightweight core with heavy features loaded as optional plugins\n- Production integration with DeepSeek Harness / DSH architecture\n\nHyperMarkdown is the official Markdown component used by Æven and integrates with the DeepSeek Harness (DSH) architecture. It was built around the demands of an agent harness—long answers, dense code, wide tables, reasoning traces, and many small deltas—not adapted from a finished-document renderer after the fact.\n\nThe captured AI workloads in the benchmark suite come from that environment. They exercise the content shapes a renderer encounters in a real model response, with controlled chunk sizes that keep comparisons reproducible.\n\n```\nnpm install @aeven-ai/hypermarkdown\n```\n\nReact 18 or 19 is required as a peer dependency.\n\nImport the component stylesheet once in your application entry point:\n\n```\nimport \"@aeven-ai/hypermarkdown/styles.css\";\n```\n\nThen import the component:\n\n``` js\nimport {\n  HyperMarkdown,\n  type HyperMarkdownHandle,\n} from \"@aeven-ai/hypermarkdown\";\n```\n\nUse the `md`\n\nprop when the complete document already exists:\n\n```\n<HyperMarkdown md={markdown} />\n```\n\nUpdating `md`\n\nreplaces the document. This mode works naturally for stored\nmessages, previews, and server-rendered content.\n\nMount one renderer for the active response and write each incoming **delta**\nto its imperative handle:\n\n``` js\nimport { useRef } from \"react\";\nimport {\n  HyperMarkdown,\n  type HyperMarkdownHandle,\n} from \"@aeven-ai/hypermarkdown\";\n\nfunction Chat() {\n  const renderer = useRef<HyperMarkdownHandle>(null);\n\n  async function generate(prompt: string) {\n    // Reuse the mounted component for a new response.\n    renderer.current?.reset();\n\n    const deltas = await createResponseStream(prompt);\n\n    try {\n      for await (const delta of deltas) {\n        renderer.current?.write(delta);\n      }\n    } finally {\n      // Flush the final open paragraph, fence, list, table, or reasoning block.\n      renderer.current?.write(\"\", true);\n    }\n  }\n\n  return (\n    <HyperMarkdown\n      ref={renderer}\n      streaming\n      animation\n    />\n  );\n}\n```\n\n`createResponseStream()`\n\nabove represents your SDK or transport. It only needs\nto yield the text fragments produced since the previous event.\n\n`write()`\n\nappends. Pass the new fragment exactly once:\n\n```\nrenderer.current?.write(delta);     // correct\nrenderer.current?.write(fullText);  // wrong: repeats everything already written\n```\n\nWhen the stream ends, finalize it exactly once. Either form is valid:\n\n```\nrenderer.current?.write(\"\", true);          // separate finalization\nrenderer.current?.write(lastDelta, true);   // final delta and finalization\n```\n\nFinalization matters even when the visible text looks complete: it settles the active frontier and lets the renderer finish incomplete-block bookkeeping.\n\nBefore using the same mounted component for another response, call `reset()`\n\n.\nKeep the component and its `ref`\n\nmounted during a response; changing its React\n`key`\n\ncreates a new, empty renderer.\n\nSome APIs emit `\"Hello\"`\n\n, then `\"Hello world\"`\n\n, rather than `\"Hello\"`\n\n, then\n`\" world\"`\n\n. Convert those snapshots to deltas at the boundary:\n\n``` js\nlet previous = \"\";\n\nfunction startSnapshotStream() {\n  previous = \"\";\n  renderer.current?.reset();\n}\n\nfunction writeSnapshot(next: string, final = false) {\n  const handle = renderer.current;\n  if (!handle) return;\n\n  if (!next.startsWith(previous)) {\n    // The provider revised an earlier prefix. Rebuild from the new snapshot.\n    handle.reset();\n    previous = \"\";\n  }\n\n  handle.write(next.slice(previous.length), final);\n  previous = next;\n}\n```\n\nCall `startSnapshotStream()`\n\nbefore the first snapshot of each response.\n\nDo not put the growing Markdown string in React state just to feed it back as a prop on every token. In streaming mode, HyperMarkdown owns that buffer so your component tree does not have to.\n\nHyperMarkdown supports Next.js server rendering and hydration. Its public\ncomponent entry includes `\"use client\"`\n\n, so an App Router Server Component can\nimport it directly. The Client Component is still prerendered into the initial\nHTML and hydrated in the browser.\n\nImport the stylesheet once in the root layout:\n\n```\n// app/layout.tsx\nimport \"@aeven-ai/hypermarkdown/styles.css\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\nThen render finished Markdown from a Server Component:\n\n``` js\n// app/page.tsx\nimport { HyperMarkdown } from \"@aeven-ai/hypermarkdown\";\n\nexport default async function Page() {\n  const markdown = await loadMarkdown();\n  return <HyperMarkdown md={markdown} />;\n}\n```\n\nThe `md`\n\nprop is serializable and can cross the Server Component boundary.\nCreate plugins, component overrides, refs, and callbacks inside a Client\nComponent because they contain functions. Streaming through the imperative\nhandle also belongs in a Client Component.\n\nHyperMarkdown does not normally need `dynamic(..., { ssr: false })`\n\n; disabling\nSSR removes the rendered Markdown from the initial HTML.\n\nSee the full [SSR, hydration, and Next.js guide](https://aeven-ai.github.io/HyperMarkdown/docs/ssr)\nfor App Router plugins and streaming, Pages Router SSR, and hydration-mismatch\nguidance.\n\nFor finished documents, the migration is a component swap:\n\n```\n// Before\n<ReactMarkdown>{markdown}</ReactMarkdown>\n\n// After\n<HyperMarkdown md={markdown} />\n```\n\nFor streaming, the architectural change is more important. A typical prop-based loop rebuilds an accumulated string and reparses it on every chunk:\n\n```\n// Before: full document goes back through React on every delta.\nconst [markdown, setMarkdown] = useState(\"\");\n\nfor await (const delta of stream) {\n  setMarkdown((current) => current + delta);\n}\n\n<ReactMarkdown>{markdown}</ReactMarkdown>\n```\n\nReplace that state loop with one stable `HyperMarkdown`\n\ninstance:\n\n``` js\n// After: only the new fragment enters the renderer.\nconst renderer = useRef<HyperMarkdownHandle>(null);\n\nfor await (const delta of stream) {\n  renderer.current?.write(delta);\n}\nrenderer.current?.write(\"\", true);\n\n<HyperMarkdown ref={renderer} streaming />\n```\n\nCommon migration mappings:\n\n| Previous pattern | HyperMarkdown |\n|---|---|\nMarkdown passed as `children` |\n`md={markdown}` for finished content |\n| Accumulated text prop updated per token | `write(delta)` with `streaming` |\n| Clear state before a new answer | `ref.current?.reset()` |\n| End-of-stream state flag | `write(\"\", true)` |\n| GFM remark plugin | Built in |\n| Custom element renderers | `components={{ ... }}` |\n| Math, highlighting, Mermaid | Optional `plugins` slots |\n| Raw HTML plugin | Built in; sanitized by default |\n\nHyperMarkdown does not accept arbitrary `remarkPlugins`\n\nor `rehypePlugins`\n\nthrough the component API. Use its typed feature plugins and component\noverrides; if you depend on a custom AST transform, verify that transform\nbefore replacing the old renderer.\n\n`MarkdownStream`\n\nremains exported for compatibility but is deprecated. Mount\n`HyperMarkdown`\n\n, hold a `HyperMarkdownHandle`\n\n, and replace direct engine calls\nwith `write(delta)`\n\n, `write(\"\", true)`\n\n, and `reset()`\n\n. The component owns the\nstore and subscribes React to it safely.\n\nMath, syntax highlighting, diagrams, and CJK-friendly emphasis are optional. Install only what your application uses:\n\n```\nnpm install katex remark-math rehype-katex\nnpm install rehype-highlight\nnpm install mermaid\nnpm install remark-cjk-friendly\njs\nimport { katexPlugin } from \"@aeven-ai/hypermarkdown/plugins/math\";\nimport { highlightPlugin } from \"@aeven-ai/hypermarkdown/plugins/code\";\nimport { mermaidPlugin } from \"@aeven-ai/hypermarkdown/plugins/mermaid\";\nimport { cjkPlugin } from \"@aeven-ai/hypermarkdown/plugins/cjk\";\nimport \"katex/dist/katex.min.css\";\n\n// Build this once. A new plugin object rebuilds the processing pipelines.\nconst plugins = {\n  math: katexPlugin(),\n  code: highlightPlugin(),\n  diagram: mermaidPlugin({ theme: \"neutral\", fontFamily: \"Inter\" }),\n  cjk: cjkPlugin(),\n};\n\n<HyperMarkdown md={markdown} plugins={plugins} />\n```\n\nEach missing plugin degrades gracefully:\n\n| Missing plugin | Behavior |\n|---|---|\n`math` |\n`$x$` remains literal text |\n`code` |\nCode blocks retain caching, controls, and line numbers but are not highlighted |\n`diagram` |\nA `mermaid` fence renders as an ordinary code block |\n`cjk` |\nStandard CommonMark emphasis rules apply |\n\nMermaid is dynamically imported. With `preload`\n\noff, loading starts when an\nopening Mermaid fence appears, overlapping the rest of the stream. Set\n`preload`\n\nwhen a view is very likely to contain diagrams and should begin the\ndownload on mount.\n\nLLM chunks end in inconvenient places. HyperMarkdown treats partial syntax as a normal state, not an error:\n\n- Half-written links, autolinks, HTML tags, and math are withheld until safe to render.\n- Emphasis resolves eagerly when the CommonMark delimiter rules make it unambiguous.\n- Open code fences, tables, and lists render their stable content while the unfinished frontier continues changing.\n- A finalized stream matches the whole-document parse across the correctness fixture suite.\n\nModel reasoning wrapped in `<think>`\n\n, `<thinking>`\n\n, or `<reasoning>`\n\nbecomes a\ncollapsible block. It stays open while tokens arrive and collapses when the\nblock finishes.\n\n```\n<think>\nChecking the constraints first.\n</think>\n\nThe answer is 42.\n```\n\nMarkdown inside the reasoning block is rendered normally. A partial opening\ntag such as `<thi`\n\nis withheld instead of flashing as text.\n\nTo place reasoning outside the answer container, provide a portal target:\n\n``` js\nconst reasoning = useRef<HTMLDivElement>(null);\n\nreturn (\n  <>\n    <div ref={reasoning} />\n    <HyperMarkdown\n      ref={renderer}\n      streaming\n      reasoningTarget={() => reasoning.current}\n    />\n  </>\n);\n```\n\nSet `controls={{ reasoning: false }}`\n\nto render the content without the\ncollapsible wrapper. Override `translations.thinking`\n\nand\n`translations.thoughtFor`\n\nto localize its labels.\n\nMarkdown produced by a model is untrusted input. HyperMarkdown defaults to\n`html=\"sanitize\"`\n\n: raw HTML is parsed, then cleaned before math, syntax\nhighlighting, diagrams, or animation run. Scripts, styles, iframes, forms, and\nevent-handler attributes are removed.\n\nLinks and images are checked separately. By default, `http`\n\n, `https`\n\n,\n`mailto`\n\n, and `tel`\n\nprotocols are allowed, as are `data:`\n\nimages.\n\nChoose the policy explicitly when needed:\n\n```\n<HyperMarkdown md={markdown} html=\"literal\" />\n```\n\n`html` mode |\nBehavior |\n|---|---|\n`\"sanitize\"` |\nDefault. Parse raw HTML and remove anything outside the schema. |\n`\"literal\"` |\nRender raw HTML as visible text. Strongest option for untrusted output. |\n`\"raw\"` |\nParse without sanitization. Use only for content you control. |\n\nTo widen the default policy without disabling it:\n\n```\n<HyperMarkdown\n  md={markdown}\n  allowedTags={{ mention: [\"data-user-id\"] }}\n  linkSafety={{ allowedLinkPrefixes: [\"https://docs.example.com/\"] }}\n/>\n```\n\n`sanitize={false}`\n\nis retained for compatibility and selects raw mode when\n`html`\n\nis not set. Prefer the clearer `html`\n\nprop in new code.\n\nThe shipped stylesheet is scoped under `.hypermarkdown`\n\n. Customize it with\nCSS variables rather than overriding internal selectors:\n\n```\n.assistant-message {\n  --hm-font: Inter, sans-serif;\n  --hm-font-mono: \"Geist Mono\", monospace;\n  --hm-color: #171717;\n  --hm-background: #f5f5f5;\n  --hm-link-color: #2563eb;\n  --hm-radius: 16px;\n  --hm-max-width: 100%;\n}\n<HyperMarkdown className=\"assistant-message\" md={markdown} />\n```\n\nThe root always receives `hypermarkdown`\n\n; `className`\n\nis added alongside it.\nKaTeX requires its own stylesheet when the math plugin is enabled.\n\nReplace rendered tags with stable React component references:\n\n``` js\nconst components = {\n  a: AppLink,\n  img: ProxiedImage,\n  code: Code,\n};\n\n<HyperMarkdown md={markdown} components={components} />\n```\n\nHyperMarkdown already provides specialized renderers for links, images, code blocks, tables, and diagrams. An override wins over the built-in component. Keep the object and component functions stable—recreating them on every render can remount rendered elements.\n\n| Prop | Type | Description |\n|---|---|---|\n`md` |\n`string` |\nFinished Markdown. Ignored while `streaming` is true. |\n`streaming` |\n`boolean` |\nReceive content through the imperative handle. |\n`animation` |\n`boolean` |\nFade arriving words in. |\n`plugins` |\n`PluginConfig` |\nOptional math, code, diagram, and CJK plugins. |\n`preload` |\n`boolean` |\nBegin loading the configured diagram engine on mount. |\n`components` |\n`RendererComponents` |\nStable tag-to-component overrides. |\n`html` |\n`\"sanitize\" | \"literal\" | \"raw\"` |\nRaw HTML policy. Defaults to `\"sanitize\"` . |\n`allowedTags` |\n`Record<string, string[]>` |\nAdditional sanitized tags and attributes. |\n`linkSafety` |\n`LinkSafetyConfig` |\nAllowed URL protocols and prefixes. |\n`reasoningTarget` |\n`HTMLElement | null | () => HTMLElement | null` |\nOptional portal target for reasoning. |\n`controls` |\n`ControlsConfig` |\nConfigure or hide reasoning, code, table, and diagram controls. |\n`translations` |\n`Partial<Translations>` |\nOverride UI strings. |\n`icons` |\n`Partial<IconMap>` |\nOverride toolbar icons with inline SVG strings. |\n`lineNumbers` |\n`boolean` |\nShow code line numbers. Defaults to `true` . |\n`codeBlockMaxHeight` |\n`number | string` |\nHeight at which code blocks scroll; numbers are pixels. |\n`tableMaxHeight` |\n`number | string` |\nHeight at which tables scroll; numbers are pixels. |\n`scrollDown` |\n`() => void` |\nRuns after each committed update for host scroll management. |\n`onFullscreenChange` |\n`(fullscreen: boolean) => void` |\nReports code, table, or diagram fullscreen changes. |\n`onAlert` |\n`(alert: HyperMarkdownAlert) => void` |\nLets the host present block alerts. |\n`className` |\n`string` |\nAdditional class on the `.hypermarkdown` root. |\n\n| Member | Description |\n|---|---|\n`write(delta, finalize?)` |\nAppend one delta; pass `true` once at end of stream. |\n`reset()` |\nDiscard rendered content and start a new stream. |\n`store` |\nThe component's rendering store for advanced integrations. |\n`stream` |\nDeprecated alias for `store` . |\n\nCommonMark plus GitHub Flavored Markdown: tables, task lists, strikethrough, autolinks, and footnotes. Optional plugins add KaTeX math, Mermaid diagrams, syntax highlighting, and CJK-friendly emphasis. Raw HTML is supported under the selected safety policy.\n\n```\nnpm install\nnpm test\nnpm run test:coverage\nnpm run typecheck\nnpm run lint\nnpm run build\nnpm run benchmark\nnpm run website:dev\n```\n\nThe documentation site and playground live in `website/`\n\nand deploy to\n[GitHub Pages](https://aeven-ai.github.io/HyperMarkdown/) from\n`.github/workflows/pages.yml`\n\n. Set the repository Pages source to **GitHub\nActions**.\n\nPull requests and pushes to `main`\n\nrun lint, typecheck (React 18 and 19), unit\nand full coverage, and a production build. Both coverage gates are 100%\nstatements, lines, functions, and branches. Coverage reports are uploaded as\nartifacts and posted on pull requests. Mark the CI job as a required status\ncheck on `main`\n\nif you want GitHub to block merges on a red build.\n\nThe correctness suite compares finished streaming output with whole-document rendering across the fixture corpus. The benchmark harness validates DOM output and measures chunk processing plus synchronous React commits.\n\nPublishing to npm is triggered by a GitHub Release whose tag matches\n`package.json`\n\n:\n\n- Bump\n`version`\n\nin`package.json`\n\n(and the lockfile). - Commit, tag\n`vX.Y.Z`\n\n, and push. - Create a GitHub Release from that tag.\n\nThe [publish workflow](/Aeven-AI/HyperMarkdown/blob/main/.github/workflows/publish.yml) re-runs the CI gates and\npublishes `@aeven-ai/hypermarkdown`\n\nwith [npm trusted publishing](https://docs.npmjs.com/trusted-publishers)\n(OIDC, no long-lived token, provenance generated automatically). Do not set\n`NODE_AUTH_TOKEN`\n\nor `setup-node`\n\n's `registry-url`\n\n; those force classic auth\nand the publish fails with `E404`\n\n.\n\nOne-time setup on [npmjs.com](https://www.npmjs.com/package/@aeven-ai/hypermarkdown)\n→ package Settings → Trusted Publisher:\n\n| Field | Value |\n|---|---|\n| Organization or user | `Aeven-AI` |\n| Repository | `HyperMarkdown` |\n| Workflow filename | `publish.yml` |\n| Environment name | `npm` |\n| Allowed actions | `npm publish` |\n\nCreate a GitHub Environment named `npm`\n\nif it does not exist. Optional\nrequired reviewers on that environment add a human approval step before npm\nsees the package.\n\n## Project layout\n\n```\nindex.tsx                     public React component and types\n\nlib/\n  renderer.tsx                buffers, caching, and incremental rendering\n  processors.ts               Markdown and HTML processing pipelines\n  stream/                     block detection and boundaries\n  repair/                     safe handling of incomplete inline syntax\n  cache/                      code-line, table-row, and list-item caches\n  plugins/                    optional math, code, Mermaid, and CJK adapters\n  code/  table/  mermaid/     rich block renderers and controls\n  reasoning/                  streamed reasoning UI\n  sanitize.ts                 HTML and URL safety policy\n\nstyles/hypermarkdown.scss     scoped component stylesheet\ntests/                        correctness, streaming, API, UI, and security\nbenchmarks/                   fixtures, competing renderers, and results\nexample/                      browser example using the built package\n```\n\nMIT", "url": "https://wpnews.pro/news/show-hn-hypermarkdown-streaming-markdown-renderer-for-react", "canonical_source": "https://github.com/Aeven-AI/HyperMarkdown", "published_at": "2026-09-01 09:43:41+00:00", "updated_at": "2026-09-01 09:52:12.370550+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Æven", "HyperMarkdown", "Markstream", "Streamdown", "DeepSeek Harness", "react-markdown", "React", "npm"], "alternates": {"html": "https://wpnews.pro/news/show-hn-hypermarkdown-streaming-markdown-renderer-for-react", "markdown": "https://wpnews.pro/news/show-hn-hypermarkdown-streaming-markdown-renderer-for-react.md", "text": "https://wpnews.pro/news/show-hn-hypermarkdown-streaming-markdown-renderer-for-react.txt", "jsonld": "https://wpnews.pro/news/show-hn-hypermarkdown-streaming-markdown-renderer-for-react.jsonld"}}