AI News HubLIVE
站內改寫5 分鐘閱讀

待翻譯:Show HN: HyperMarkdown, streaming Markdown renderer for React

AI 服務暫時不可用,以下為來源摘要,待恢復後補全翻譯:Uh oh! There was an error while loading. Please reload this page. Notifications You must be signed in to change notification settings Fork 0 Star 0 BranchesTags Open more actions menu Latest commit History 53 Commits 53…

來源Hacker News AI作者: airwave

AI 服務暫時不可用,以下為來源正文,待恢復後補全翻譯。

Uh oh! There was an error while loading. Please reload this page. Notifications You must be signed in to change notification settings Fork 0 Star 0 BranchesTags Open more actions menu Latest commit History 53 Commits 53 Commits Folders and files NameName Last commit message Last commit date .github/workflows .github/workflows benchmarks benchmarks example example lib lib scripts scripts styles styles tests tests website website .gitignore .gitignore LICENSE LICENSE README.md README.md eslint.config.js eslint.config.js index.tsx index.tsx package-lock.json package-lock.json package.json package.json tsconfig.app.json tsconfig.app.json tsconfig.json tsconfig.json tsconfig.node.json tsconfig.node.json tsconfig.react18.json tsconfig.react18.json tsconfig.react19.json tsconfig.react19.json vite.config.ts vite.config.ts vitest.unit.config.ts vitest.unit.config.ts Repository files navigation Ridiculously fast Markdown for React and AI. Parse the change. Not the conversation. HyperMarkdown 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. Performance 1.6×–10.6× faster than the nearest streaming renderer across our benchmark suite. Workload HyperMarkdown Markstream Streamdown DeepSeek Harness react-markdown Large code block 216 ms 769 ms 3,242 ms 4,416 ms 2,054 ms Mixed prose 153 ms 313 ms 559 ms 249 ms 2,629 ms Captured AI code stream (real-code-os) 659 ms 4,417 ms 12,511 ms 14,621 ms 8,008 ms Captured AI table stream (real-table-head) 654 ms 4,948 ms 11,621 ms 19,644 ms 10,236 ms Large table 874 ms 9,276 ms 33,142 ms 55,918 ms 29,747 ms The captured model fixtures are not generated stress cases: their content is real AI output, replayed in controlled 8-character frames. HyperMarkdown renders the code stream in 659 ms versus 4,417 ms for the next closest streaming renderer, and the table stream in 654 ms versus 4,948 ms. On a large streaming table, HyperMarkdown completes the workload in under one second. Streamdown: 33 seconds DeepSeek Harness strategy: 56 seconds react-markdown: 30 seconds Same Markdown. Same stream. Very different architecture. Production React benchmark on an Apple M2 Max, including chunk processing and synchronous render/commit. Absolute timings vary; the ratios are the useful comparison. Read the methodology · View the full benchmark results Why is it so fast? Most streaming Markdown renderers optimize at the document or block level. HyperMarkdown goes further. Traditional streaming renderer new token ↓ growing active block ↓ parse the active block again ↓ render again HyperMarkdown: new token ↓ active block │ ├── settled code lines → cached ├── settled table rows → cached ├── settled list items → cached └── changing frontier → parse A 1,000-line code block does not become a 1,000-line parsing problem every time another token arrives. Completed work stays completed. Built for AI streaming Sub-block caching for code, tables, and lists Streaming-safe handling of incomplete Markdown GFM tables, task lists, autolinks, and footnotes Reasoning blocks written as , , or KaTeX math Mermaid diagrams Syntax highlighting Raw HTML with sanitization React 18 and React 19 SSR and hydration, including a Next.js App Router client boundary Lightweight core with heavy features loaded as optional plugins Production integration with DeepSeek Harness / DSH architecture Used by Æven HyperMarkdown 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. The 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. Install npm install @aeven-ai/hypermarkdown React 18 or 19 is required as a peer dependency. Import the component stylesheet once in your application entry point: import "@aeven-ai/hypermarkdown/styles.css"; Then import the component: import { HyperMarkdown, type HyperMarkdownHandle, } from "@aeven-ai/hypermarkdown"; Quick start Finished Markdown Use the md prop when the complete document already exists: Updating md replaces the document. This mode works naturally for stored messages, previews, and server-rendered content. Streaming Markdown Mount one renderer for the active response and write each incoming delta to its imperative handle: import { useRef } from "react"; import { HyperMarkdown, type HyperMarkdownHandle, } from "@aeven-ai/hypermarkdown"; function Chat() { const renderer = useRef(null); async function generate(prompt: string) { // Reuse the mounted component for a new response. renderer.current?.reset(); const deltas = await createResponseStream(prompt); try { for await (const delta of deltas) { renderer.current?.write(delta); } } finally { // Flush the final open paragraph, fence, list, table, or reasoning block. renderer.current?.write("", true); } } return ( ); } createResponseStream() above represents your SDK or transport. It only needs to yield the text fragments produced since the previous event. The delta contract write() appends. Pass the new fragment exactly once: renderer.current?.write(delta); // correct renderer.current?.write(fullText); // wrong: repeats everything already written When the stream ends, finalize it exactly once. Either form is valid: renderer.current?.write("", true); // separate finalization renderer.current?.write(lastDelta, true); // final delta and finalization Finalization matters even when the visible text looks complete: it settles the active frontier and lets the renderer finish incomplete-block bookkeeping. Before using the same mounted component for another response, call reset(). Keep the component and its ref mounted during a response; changing its React key creates a new, empty renderer. If your source emits cumulative snapshots Some APIs emit "Hello", then "Hello world", rather than "Hello", then " world". Convert those snapshots to deltas at the boundary: let previous = ""; function startSnapshotStream() { previous = ""; renderer.current?.reset(); } function writeSnapshot(next: string, final = false) { const handle = renderer.current; if (!handle) return; if (!next.startsWith(previous)) { // The provider revised an earlier prefix. Rebuild from the new snapshot. handle.reset(); previous = ""; } handle.write(next.slice(previous.length), final); previous = next; } Call startSnapshotStream() before the first snapshot of each response. Do 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. Next.js HyperMarkdown supports Next.js server rendering and hydration. Its public component entry includes "use client", so an App Router Server Component can import it directly. The Client Component is still prerendered into the initial HTML and hydrated in the browser. Import the stylesheet once in the root layout: // app/layout.tsx import "@aeven-ai/hypermarkdown/styles.css"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } Then render finished Markdown from a Server Component: // app/page.tsx import { HyperMarkdown } from "@aeven-ai/hypermarkdown"; export default async function Page() { const markdown = await loadMarkdown(); return ; } The md prop is serializable and can cross the Server Component boundary. Create plugins, component overrides, refs, and callbacks inside a Client Component because they contain functions. Streaming through the imperative handle also belongs in a Client Component. HyperMarkdown does not normally need dynamic(..., { ssr: false }); disabling SSR removes the rendered Markdown from the initial HTML. See the full SSR, hydration, and Next.js guide for App Router plugins and streaming, Pages Router SSR, and hydration-mismatch guidance. Migrating From react-markdown or another prop-based renderer For finished documents, the migration is a component swap: // Before {markdown} // After For streaming, the architectural change is more important. A typical prop-based loop rebuilds an accumulated string and reparses it on every chunk: // Before: full document goes back through React on every delta. const [markdown, setMarkdown] = useState(""); for await (const delta of stream) { setMarkdown((current) => current + delta); } {markdown} Replace that state loop with one stable HyperMarkdown instance: // After: only the new fragment enters the renderer. const renderer = useRef(null); for await (const delta of stream) { renderer.current?.write(delta); } renderer.current?.write("", true); Common migration mappings: Previous pattern HyperMarkdown Markdown passed as children md={markdown} for finished content Accumulated text prop updated per token write(delta) with streaming Clear state before a new answer ref.current?.reset() End-of-stream state flag write("", true) GFM remark plugin Built in Custom element renderers components={{ ... }} Math, highlighting, Mermaid Optional plugins slots Raw HTML plugin Built in; sanitized by default HyperMarkdown does not accept arbitrary remarkPlugins or rehypePlugins through the component API. Use its typed feature plugins and component overrides; if you depend on a custom AST transform, verify that transform before replacing the old renderer. From the legacy MarkdownStream export MarkdownStream remains exported for compatibility but is deprecated. Mount HyperMarkdown, hold a HyperMarkdownHandle, and replace direct engine calls with write(delta), write("", true), and reset(). The component owns the store and subscribes React to it safely. Optional plugins Math, syntax highlighting, diagrams, and CJK-friendly emphasis are optional. Install only what your application uses: npm install katex remark-math rehype-katex npm install rehype-highlight npm install mermaid npm install remark-cjk-friendly import { katexPlugin } from "@aeven-ai/hypermarkdown/plugins/math"; import { highlightPlugin } from "@aeven-ai/hypermarkdown/plugins/code"; import { mermaidPlugin } from "@aeven-ai/hypermarkdown/plugins/mermaid"; import { cjkPlugin } from "@aeven-ai/hypermarkdown/plugins/cjk"; import "katex/dist/katex.min.css"; // Build this once. A new plugin object rebuilds the processing pipelines. const plugins = { math: katexPlugin(), code: highlightPlugin(), diagram: mermaidPlugin({ theme: "neutral", fontFamily: "Inter" }), cjk: cjkPlugin(), }; Each missing plugin degrades gracefully: Missing plugin Behavior math $x$ remains literal text code Code blocks retain caching, controls, and line numbers but are not highlighted diagram A mermaid fence renders as an ordinary code block cjk Standard CommonMark emphasis rules apply Mermaid is dynamically imported. With preload off, loading starts when an opening Mermaid fence appears, overlapping the rest of the stream. Set preload when a view is very likely to contain diagrams and should begin the download on mount. Incomplete Markdown LLM chunks end in inconvenient places. HyperMarkdown treats partial syntax as a normal state, not an error: Half-written links, autolinks, HTML tags, and math are withheld until safe to render. Emphasis resolves eagerly when the CommonMark delimiter rules make it unambiguous. Open code fences, tables, and lists ren [truncated for AI cost control]