<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Simon He</title>
    <description>The latest articles on DEV Community by Simon He (@simonhe95).</description>
    <link>https://dev.to/simonhe95</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036641%2F01ac01be-f923-4ce0-a357-9dcbc72170d1.png</url>
      <title>DEV Community: Simon He</title>
      <link>https://dev.to/simonhe95</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/simonhe95"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Simon He</dc:creator>
      <pubDate>Tue, 03 Nov 2026 06:51:42 +0000</pubDate>
      <link>https://dev.to/simonhe95/-697</link>
      <guid>https://dev.to/simonhe95/-697</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne" class="crayons-story__hidden-navigation-link"&gt;Streaming Markdown in Vue: Rendering LLM Tokens Without Flicker&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/simonhe95" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036641%2F01ac01be-f923-4ce0-a357-9dcbc72170d1.png" alt="simonhe95 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/simonhe95" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Simon He
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Simon He
                
                
              
              &lt;div id="story-author-preview-content-4967204" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/simonhe95" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036641%2F01ac01be-f923-4ce0-a357-9dcbc72170d1.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Simon He&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne" id="article-link-4967204"&gt;
          Streaming Markdown in Vue: Rendering LLM Tokens Without Flicker
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/vue"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;vue&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>javascript</category>
      <category>llm</category>
      <category>streaming</category>
      <category>vue</category>
    </item>
    <item>
      <title>Streaming Markdown in Vue: Rendering LLM Tokens Without Flicker</title>
      <dc:creator>Simon He</dc:creator>
      <pubDate>Tue, 03 Nov 2026 06:51:08 +0000</pubDate>
      <link>https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne</link>
      <guid>https://dev.to/simonhe95/streaming-markdown-in-vue-rendering-llm-tokens-without-flicker-4fne</guid>
      <description>&lt;p&gt;Every AI chat UI hits the same wall. Tokens arrive over SSE, you do &lt;code&gt;content += token&lt;/code&gt;, and a computed property calls &lt;code&gt;marked.parse(content)&lt;/code&gt; again. For a short answer it works. For a 3,000-token answer with a table, a fenced code block, and a Mermaid diagram, the message area re-renders dozens of times per second. The result is flicker, jumping scroll position, diagrams that vanish until the closing fence arrives, and a main thread that stays busy long after the stream ends.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/Simon-He95/markstream-vue" rel="noopener noreferrer"&gt;markstream-vue&lt;/a&gt;, a streaming Markdown renderer built for exactly this case. This post walks through why the usual renderer pipeline struggles with token streams, and what a streaming-first renderer does differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't Markdown. It's the timing.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;marked&lt;/code&gt;, &lt;code&gt;markdown-it&lt;/code&gt;, and &lt;code&gt;react-markdown&lt;/code&gt; are excellent at rendering a &lt;strong&gt;finished&lt;/strong&gt; document. Their contract is: give me valid Markdown, I give you HTML or a component tree.&lt;/p&gt;

&lt;p&gt;An LLM stream violates that contract constantly. Mid-stream you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an unclosed &lt;code&gt;**bold&lt;/code&gt; run,&lt;/li&gt;
&lt;li&gt;a code fence with no closing backticks,&lt;/li&gt;
&lt;li&gt;half a table,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$$&lt;/code&gt; math with no closing delimiter,&lt;/li&gt;
&lt;li&gt;a Mermaid block that only becomes parseable at the very end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you re-parse and re-render the full string on every token, you pay for the entire document each time. If you pass partial Markdown straight to a finished-document renderer, you get either broken output or a loading state that flashes on every chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What streaming-first actually changes
&lt;/h2&gt;

&lt;p&gt;Markstream keeps the incomplete state as a first-class idea instead of an error case.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Two render modes for two different UX goals
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Virtual window (default)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-live-nodes="220"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long answers, docs, chat history — steady scrolling and memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental batches&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-live-nodes="0"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The "typing" feel — new content flows in small slices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The virtual window keeps a sliding set of mounted nodes, so a 10,000-block document doesn't mean 10,000 live DOM nodes. Incremental mode disables virtualization and renders in small batches, which is usually what you want for a chat bubble that is actively growing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Heavy blocks settle progressively
&lt;/h3&gt;

&lt;p&gt;Mermaid and KaTeX are optional peers, not bundled dependencies. When they are installed, diagrams can render as soon as the syntax is complete, and update when later chunks change them — rather than blocking the whole message until the stream ends.&lt;/p&gt;

&lt;p&gt;Code blocks are the other hot spot. In v2, the enhanced code surface comes from the optional &lt;code&gt;stream-diffs&lt;/code&gt; peer; without it you get a plain &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; fallback. That keeps the default install small and lets you opt into the heavier diff/highlighting surface only where you need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;final&lt;/code&gt; is a real state, not a guess
&lt;/h3&gt;

&lt;p&gt;Most flicker bugs come from not knowing whether the stream is done. Markstream takes a &lt;code&gt;final&lt;/code&gt; prop. While it is &lt;code&gt;false&lt;/code&gt;, the parser assumes unfinished constructs are still loading. When it flips to &lt;code&gt;true&lt;/code&gt;, the renderer invalidates the streaming cache and reparses with final semantics, so a trailing &lt;code&gt;$$&lt;/code&gt; or an unclosed fence doesn't stay stuck in a loading state forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add markstream-vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MarkdownRender&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markstream-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markstream-vue/index.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;MarkdownRender&lt;/span&gt; &lt;span class="na"&gt;:content=&lt;/span&gt;&lt;span class="s"&gt;"markdown"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole integration for a read-only surface. For an AI chat message, add the stream state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onBeforeUnmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MarkdownRender&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markstream-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markstream-vue/index.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;onBeforeUnmount&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;MarkdownRender&lt;/span&gt;
    &lt;span class="na"&gt;:content=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;
    &lt;span class="na"&gt;:final=&lt;/span&gt;&lt;span class="s"&gt;"final"&lt;/span&gt;
    &lt;span class="na"&gt;:max-live-nodes=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
    &lt;span class="na"&gt;:render-batch-size=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt;
    &lt;span class="na"&gt;:render-batch-delay=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt;
    &lt;span class="na"&gt;:fade=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;
    &lt;span class="na"&gt;:typewriter=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;:max-live-nodes="0"&lt;/code&gt; selects incremental mode. The batch props control how much work is allowed per frame, which is the difference between a smooth typewriter effect and one that jumps in 2 KB chunks.&lt;/p&gt;

&lt;p&gt;If you already parse Markdown on the server or in a worker, you can skip client-side parsing entirely and pass typed nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getMarkdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseMarkdownToStructure&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markstream-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMarkdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseMarkdownToStructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;final&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;MarkdownRender&lt;/span&gt; &lt;span class="na"&gt;:nodes=&lt;/span&gt;&lt;span class="s"&gt;"nodes"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is also the escape hatch for SSR and hydration: parse once on the server, serialize the nodes, and keep streaming later chunks on the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting Vue components inside Markdown
&lt;/h2&gt;

&lt;p&gt;This is the part that made the library worth building for me. A chat answer often needs a callout, a button, or a chart — something that isn't expressible in plain Markdown.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setCustomComponents&lt;/code&gt; maps a marker to a Vue component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setCustomComponents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markstream-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;setCustomComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;CALLOUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Callout.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then emit the marker in Markdown and scope the renderer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[[CALLOUT:warning title="Heads up" body="This response was truncated."]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;MarkdownRender&lt;/span&gt; &lt;span class="na"&gt;:content=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt; &lt;span class="na"&gt;custom-id=&lt;/span&gt;&lt;span class="s"&gt;"chat"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same mechanism handles custom HTML tags such as &lt;code&gt;thinking&lt;/code&gt;, with the original payload kept on the node.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;em&gt;not&lt;/em&gt; to use it
&lt;/h2&gt;

&lt;p&gt;I would rather you pick the right tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static docs, blog posts, changelogs&lt;/strong&gt; — &lt;code&gt;marked&lt;/code&gt; or &lt;code&gt;markdown-it&lt;/code&gt; is simpler and smaller. There is no stream to optimize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need every ecosystem plugin&lt;/strong&gt; — the unified/remark ecosystem is hard to beat for finished documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want zero optional peers&lt;/strong&gt; — Markstream works, but Mermaid, KaTeX, and the enhanced code surface are opt-in for a reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The value shows up when Markdown is still changing while the user is reading it. That is a different rendering problem from "render this file".&lt;/p&gt;

&lt;h2&gt;
  
  
  Other frameworks
&lt;/h2&gt;

&lt;p&gt;The same core is available outside Vue: &lt;code&gt;markstream-react&lt;/code&gt; (React / Next.js), &lt;code&gt;markstream-solid&lt;/code&gt;, &lt;code&gt;markstream-svelte&lt;/code&gt;, &lt;code&gt;markstream-angular&lt;/code&gt;, &lt;code&gt;markstream-vue2&lt;/code&gt;, plus &lt;code&gt;stream-markdown-parser&lt;/code&gt; for parsing without a UI layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live Vue playground: &lt;a href="https://markstream-vue.simonhe.me/" rel="noopener noreferrer"&gt;https://markstream-vue.simonhe.me/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Shareable repro / test page: &lt;a href="https://markstream-vue.simonhe.me/test" rel="noopener noreferrer"&gt;https://markstream-vue.simonhe.me/test&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://markstream.simonhe.me/frameworks/vue" rel="noopener noreferrer"&gt;https://markstream.simonhe.me/frameworks/vue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Simon-He95/markstream-vue" rel="noopener noreferrer"&gt;https://github.com/Simon-He95/markstream-vue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/markstream-vue" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/markstream-vue&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a streaming UI that flickers, the test page lets you paste a repro and share the URL — that is the fastest way to show me the bug.&lt;/p&gt;

&lt;p&gt;Prefer the short version first? I wrote an earlier, shorter post on why streaming AI chat needs a renderer for incomplete Markdown: &lt;a href="https://dev.to/simonhe95/why-streaming-ai-chat-needs-a-renderer-for-incomplete-markdown-4754"&gt;https://dev.to/simonhe95/why-streaming-ai-chat-needs-a-renderer-for-incomplete-markdown-4754&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I am the maintainer of markstream-vue, so read the comparison with the usual grain of salt. The library is MIT-licensed, and the optional peers are the only paid-in-bundle-size trade-offs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>ai</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why streaming AI chat needs a renderer for incomplete Markdown</title>
      <dc:creator>Simon He</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:56:55 +0000</pubDate>
      <link>https://dev.to/simonhe95/why-streaming-ai-chat-needs-a-renderer-for-incomplete-markdown-4754</link>
      <guid>https://dev.to/simonhe95/why-streaming-ai-chat-needs-a-renderer-for-incomplete-markdown-4754</guid>
      <description>&lt;p&gt;When an AI answer arrives token by token, it spends most of its life as an &lt;strong&gt;incomplete Markdown document&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A code fence may be open, a table may have only its header, a formula may be half-written, or a nested list may still be taking shape. Most Markdown renderers are optimized for complete documents, so an AI chat interface can flicker between raw syntax and formatted output while the answer streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a streaming-aware renderer needs to handle
&lt;/h2&gt;

&lt;p&gt;A useful renderer for this state should keep partially generated content readable without pretending it is already final:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserve stable output while an open code block is still receiving tokens;&lt;/li&gt;
&lt;li&gt;render progressively without repeatedly rebuilding a long response;&lt;/li&gt;
&lt;li&gt;handle Markdown structures that arrive in pieces, including tables, lists, math, and diagrams;&lt;/li&gt;
&lt;li&gt;work with server rendering and the framework used by the chat interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Markstream
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;strong&gt;Markstream&lt;/strong&gt;, an MIT-licensed renderer focused on this exact UI problem. It is intended for AI chat and LLM streaming interfaces, and has integrations for Vue/Nuxt, React/Next.js, Svelte/SvelteKit, and Angular.&lt;/p&gt;

&lt;p&gt;It supports streaming code blocks, Mermaid diagrams, KaTeX, long responses, SSR, and safe HTML rendering. The goal is simple: keep an answer usable while it is still being written.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://markstream-vue.simonhe.me/" rel="noopener noreferrer"&gt;https://markstream-vue.simonhe.me/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source code: &lt;a href="https://github.com/Simon-He95/markstream-vue" rel="noopener noreferrer"&gt;https://github.com/Simon-He95/markstream-vue&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build streaming AI interfaces, I would be interested in the Markdown edge cases that still feel awkward in your product.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vue</category>
      <category>react</category>
      <category>svelte</category>
    </item>
  </channel>
</rss>
