Streaming code block renderer for AI Markdown
Code fences are one of the most visible failure points in AI chat. During generation, the opening fence often arrives long before the closing fence, and the language tag or code body may still be changing. Markstream handles that as a streaming state instead of assuming the Markdown is complete.
Problem
A static Markdown renderer can make an unfinished code fence consume everything below it. In AI chat, that means normal paragraphs, tables, and follow-up text may briefly become part of the code block while the model is still writing.
```ts
export function answer() {
return "still streaming"The renderer needs to keep this state readable and avoid expensive highlighter churn until the fence is stable.
Renderer choices
| Renderer | Best for | Notes |
|---|---|---|
Monaco CodeBlockNode | interactive or large code blocks | Heavier, editor-like, good for rich code UX |
Shiki MarkdownCodeBlockNode | read-only highlighted code | Lighter than Monaco, good for docs and chat |
Plain pre | mobile or strict bundle budgets | Most predictable fallback |
Minimal Vue setup
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
defineProps<{
content: string
isDone: boolean
}>()
</script>
<template>
<MarkdownRender
mode="chat"
:content="content"
:final="isDone"
:fade="false"
/>
</template>For a lightweight Shiki renderer, install stream-markdown and scope the code block override to this chat surface:
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
setCustomComponents('chat-code-blocks', {
code_block: MarkdownCodeBlockNode,
})<MarkdownRender
custom-id="chat-code-blocks"
mode="chat"
:content="content"
:final="isDone"
:fade="false"
/>For mobile WebViews or conservative bundles, use plain pre rendering:
<MarkdownRender
:content="content"
:final="isDone"
:render-code-blocks-as-pre="true"
/>Streaming states to test
- Opening fence with no close fence
- Language tag that changes from empty to
ts,tsx,vue, ordiff - Long code block streamed in small chunks
- Diff fences with added and removed lines
- Code followed by tables, Mermaid, or math before the close fence arrives
Performance notes
- Batch token updates before rendering; do not commit every byte from an SSE or WebSocket stream.
- Keep
fadedisabled in chat surfaces to avoid animation restarts. - Use viewport priority or virtualization when a long answer contains many code blocks.
- Prefer
render-code-blocks-as-preon mobile if users only need to read code.