Cómo renderizar Markdown en streaming desde redes neuronales sin parpadeos ni bloqueos
Cuando conectas por primera vez la salida en streaming de un modelo de lenguaje al frontend, un renderizador de Markdown normal convierte la página casi inmediatamente en una pesadilla. Bibliotecas estándar como markdown-it o marked fueron diseñadas para documentos estáticos listos. Si les alimentas con un stream de tokens sin procesar a través de SSE o WebSocket, re-analizan todo el texto en cada fragmento, redibujan el árbol DOM y afectan el desplazamiento.
En este punto, la interfaz comienza a parpadear de manera notable. El resaltado de sintaxis parpadea, los bloques de código sin terminar rompen el marcado debajo, y las fórmulas sin cerrar se quedan atascadas en un estado de carga infinito. El repositorio markstream-vue resuelve este frustrante problema.
Qué hay bajo el capó de la biblioteca
El proyecto comenzó como un componente especializado para Vue 3, pero con el tiempo el autor dividió la arquitectura en un núcleo stream-markdown-parser y adaptadores para diferentes frameworks. Ahora existen paquetes listos para usar para Vue 3, Nuxt, React, Next.js, Svelte 5, Angular e incluso el legacy Vue 2.
La tarea principal del renderizador es mantener el DOM estable durante las frecuentes micro-actualizaciones del texto. La biblioteca analiza el stream de manera incremental, comprende los estados intermedios de etiquetas sin cerrar y actualiza únicamente los nodos modificados, dejando el resto de la página sin cambios.
Modos de funcionamiento y gestión de carga
La biblioteca tiene dos enfoques de renderizado fundamentalmente diferentes que se pueden alternar mediante la prop mode.
El modo mode="chat" está diseñado para chats de IA. En él, el renderizador agrupa los tokens entrantes en lotes pequeños y los muestra con un efecto de escritura fluido. Al mismo tiempo, las animaciones de opacidad innecesarias se deshabilitan para que la interfaz no tiemble con cada nueva palabra.
Si necesitas mostrar un longread o documentación generada muy extensa, es mejor habilitar la virtualización mediante mode="docs". El renderizador mantiene una ventana fija de elementos en el árbol DOM activo (aproximadamente 220 nodos por defecto). Esto mantiene el consumo de memoria del navegador en un nivel estable y previene bloqueos al desplazarte por conversaciones largas.
<script setup lang="ts">
import { ref } from 'vue'
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
const message = ref('')
const isDone = ref(false)
// Получаем чанки через EventSource или fetch
const eventSource = new EventSource('/api/chat')
eventSource.onmessage = (event) => {
message.value += event.data
}
eventSource.addEventListener('done', () => {
isDone.value = true
eventSource.close()
})
</script>
<template>
<MarkdownRender
mode="chat"
:content="message"
:final="isDone"
smooth-streaming="auto"
:fade="false"
/>
</template>
La prop final es crítica al trabajar con un stream. Mientras final="false" sea true, el parser tolera tranquilamente construcciones cortadas a mitad de palabra. En cuanto llega la señal de completitud, el renderizador limpia la caché del stream y lleva el marcado a su forma final.
Trabajo con bloques complejos
Los parsers normales tropiezan con diagramas de Mermaid o fórmulas de KaTeX si la sintaxis aún no se ha escrito completamente. En markstream, este momento está pensado hasta el más mínimo detalle.
Diagramas de Mermaid y fórmulas
Las dependencias pesadas como mermaid y katex no están incluidas en el bundle principal. Las instalas como peer dependencies y las activas llamando funciones:
import { enableKatex, enableMermaid } from 'markstream-vue'
import 'katex/dist/katex.min.css'
enableMermaid()
enableKatex()
Los diagramas de Mermaid se analizan progresivamente. Si el gráfico aún está siendo escrito por el modelo, el renderizador muestra un placeholder limpio en lugar de un error de sintaxis en la consola. Para KaTeX, puedes delegar el análisis de fórmulas a un Web Worker separado a través de CDN, para que las expresiones matemáticas pesadas no bloqueen el hilo principal de la interfaz en absoluto.
Bloques de código y diffs
En la versión 2.0, los desarrolladores abandonaron el pesado editor Monaco en favor de la integración con stream-diffs. Ahora puedes mostrar diffs de archivos interactivos directamente en el stream, cambiar entre temas claros y oscuros, y configurar las alturas de los bloques.
<template>
<MarkdownRender
:content="content"
:is-dark="true"
:code-block-props="{
theme: { light: 'vitesse-light', dark: 'vitesse-dark' }
}"
/>
</template>
Componentes Vue personalizados dentro de Markdown
A veces los modelos generan etiquetas no estándar, por ejemplo <thinking> para una cadena de razonamiento o shortcodes personalizados para invocar botones y widgets. Puedes interceptarlos y reemplazarlos con componentes Vue completos:
import { setCustomComponents } from 'markstream-vue'
setCustomComponents('chat-scope', {
CALLOUT: () => import('./components/Callout.vue'),
THINKING: () => import('./components/ThinkingAccordion.vue'),
})
En la plantilla, solo necesitas especificar el mismo identificador:
<MarkdownRender
:content="message"
custom-id="chat-scope"
:custom-html-tags="['thinking']"
/>
Renderizado del lado del servidor y transferencia de estado
Si estás construyendo una app con Nuxt o Next.js, no tienes que ejecutar el parser en el cliente desde cero. El documento puede ser analizado en el servidor en una estructura de nodos tipados:
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown()
const nodes = parseMarkdownToStructure(rawMarkdown, md, { final: true })
El componente cliente acepta nodos listos a través de la prop :nodes="nodesFromServer", lo que proporciona una hidratación rápida sin desajustes de layout. Si necesitas continuar el streaming después de la carga inicial de la página, el cliente simplemente toma el buffer y analiza las nuevas porciones.
Escenarios prácticos
La biblioteca cubre varias tareas comunes de desarrollo frontend a la vez:
- Interfaces de diálogo con modelos de lenguaje grandes, donde es importante eliminar el parpadeo y el temblor de pantalla.
- Sistemas de revisión de código y generación de parches con visualización de diffs durante la generación.
- Bases de conocimiento y paneles de changelog con carga dinámica de secciones y componentes interactivos.
- Páginas de documentación técnica con fórmulas y diagramas complejos.
Resumen
Si tu proyecto muestra archivos Markdown estáticos desde una carpeta local, un markdown-it probado lo manejará sin complicaciones innecesarias. Pero si estás trabajando con un stream de tokens en vivo desde un LLM, moviendo una interfaz de chat a la web, o cansado de luchar contra el lag al renderizar respuestas largas, la biblioteca definitivamente merece un lugar en tus dependencias.
Elimina docenas de problemas de streaming no obvios y ahorra mucho tiempo en escribir tus propios workarounds alrededor de los parsers. Para un inicio rápido, puedes consultar el playground oficial en línea o desplegar un entorno de prueba a través de StackBlitz.
Proyectos relacionados