Jak renderować strumieniowy Markdown z sieci neuronowych bez migotania i zacięć
Gdy po raz pierwszy połączysz strumieniowe wyjście z modelu językowego z frontendem, zwykły renderer Markdown niemal natychmiast zamienia stronę w koszmar. Standardowe biblioteki takie jak markdown-it lub marked zostały zaprojektowane do gotowych dokumentów statycznych. Jeśli nakarmisz je surowym strumieniem tokenów przez SSE lub WebSocket, przeparsowują cały tekst przy każdym fragmencie, przerysowują drzewo DOM i psują przewijanie.
W tym momencie interfejs zaczyna заметно migotać. Podświetlanie składni miga, niedokończone bloki kodu psują znaczniki poniżej, a niedomknięte formuły utykają w stanie ciągłego ładowania. Repozytorium markstream-vue rozwiązuje dokładnie ten frustrujący problem.
Co jest pod maską biblioteki
Projekt zaczął się jako wyspecjalizowany komponent dla Vue 3, ale z czasem autor podzielił architekturę na rdzeń stream-markdown-parser i adaptery dla różnych frameworków. Teraz dostępne są gotowe pakiety dla Vue 3, Nuxt, React, Next.js, Svelte 5, Angular, a nawet starszego Vue 2.
Głównym zadaniem renderera jest utrzymywanie stabilności DOM podczas częstych mikroupdate'ów tekstu. Biblioteka parsuje strumień inkrementalnie, rozumie pośrednie stany niedomkniętych znaczników i aktualizuje tylko zmienione węzły, pozostawiając resztę strony nietkniętą.
Tryby pracy i zarządzanie obciążeniem
Biblioteka ma dwa fundamentalnie różne podejścia do renderowania, które można przełączać za pomocą prop mode.
Tryb mode="chat" jest przeznaczony do czatów z AI. W nim renderer grupuje przychodzące tokeny w małe partie i wyprowadza je z płynnym efektem pisania. Jednocześnie niepotrzebne animacje przezroczystości są wyłączone, więc interfejs nie trzęsie się przy każdym nowym słowie.
Jeśli musisz wyświetlić ogromny wygenerowany długi artykuł lub dokumentację, lepiej włącz wirtualizację przez mode="docs". Renderer utrzymuje stałe okno elementów w aktywnym drzewie DOM (około 220 węzłów domyślnie). To utrzymuje zużycie pamięci przeglądarki na stałym poziomie i zapobiega zacięciom podczas przewijania długich rozmów.
<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>
Prop final jest krytyczny podczas pracy ze strumieniem. Dopóki final="false" ma wartość true, parser spokojnie toleruje konstrukcje przecięte w połowie słowa. Gdy tylko pojawi się sygnał zakończenia, renderer czyści cache strumienia i sprowadza znaczniki do ich ostatecznej formy.
Praca ze złożonymi blokami
Zwykłe parsery potykają się o diagramy Mermaid lub formuły KaTeX, jeśli składnia nie została jeszcze w pełni zapisana. W markstream ten moment jest przemyślany do najmniejszego szczegółu.
Diagramy Mermaid i formuły
Ciężkie zależności jak mermaid i katex nie są dołączone do głównego pakietu. Instalujesz je jako peer dependencies i aktywujesz wywołując funkcje:
import { enableKatex, enableMermaid } from 'markstream-vue'
import 'katex/dist/katex.min.css'
enableMermaid()
enableKatex()
Diagramy Mermaid są parsowane progresywnie. Jeśli graf jest jeszcze pisany przez model, renderer pokazuje schludny placeholder zamiast błędu składni w konsoli. W przypadku KaTeX możesz odciążyć parsowanie formuł do osobnego Web Workera przez CDN, więc ciężkie wyrażenia matematyczne w ogóle nie blokują głównego wątku interfejsu.
Bloki kodu i diffy
W wersji 2.0 deweloperzy porzucili ciężki edytor Monaco na rzecz integracji z stream-diffs. Teraz możesz wyświetlać interaktywne diffy plików bezpośrednio w strumieniu, przełączać między jasnym i ciemnym motywem oraz konfigurować wysokości bloków.
<template>
<MarkdownRender
:content="content"
:is-dark="true"
:code-block-props="{
theme: { light: 'vitesse-light', dark: 'vitesse-dark' }
}"
/>
</template>
Własne komponenty Vue wewnątrz Markdown
Czasami modele wyprowadzają niestandardowe znaczniki, na przykład <thinking> dla łańcucha rozumowania lub niestandardowe shortcodes do wywoływania przycisków i widżetów. Możesz je przechwycić i zastąpić pełnymi komponentami Vue:
import { setCustomComponents } from 'markstream-vue'
setCustomComponents('chat-scope', {
CALLOUT: () => import('./components/Callout.vue'),
THINKING: () => import('./components/ThinkingAccordion.vue'),
})
W szablonie wystarczy podać ten sam identyfikator:
<MarkdownRender
:content="message"
custom-id="chat-scope"
:custom-html-tags="['thinking']"
/>
Renderowanie po stronie serwera i przekazywanie stanu
Jeśli budujesz aplikację z Nuxt lub Next.js, nie musisz uruchamiać parsera na kliencie od zera. Dokument może być parsowany na serwerze do struktury typowanych węzłów:
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown()
const nodes = parseMarkdownToStructure(rawMarkdown, md, { final: true })
Komponent kliencki akceptuje gotowe węzły przez prop :nodes="nodesFromServer", co zapewnia szybką hydratację bez niezgodności układu. Jeśli musisz kontynuować strumieniowanie po początkowym załadowaniu strony, klient po prostu podnosi bufor i parsuje dalej nowe porcje.
Praktyczne scenariusze
Biblioteka obejmuje kilka typowych zadań frontendowych na raz:
- Interfejsy dialogowe z dużymi modelami językowymi, gdzie ważne jest wyeliminowanie migotania i trzęsienia ekranu.
- Systemy przeglądu kodu i generowania patchy z wyświetlaniem diffów w trakcie generowania.
- Bazy wiedzy i panele changeloga z dynamicznym ładowaniem sekcji i interaktywnymi komponentami.
- Strony dokumentacji technicznej z formułami i złożonymi diagramami.
Podsumowanie
Jeśli Twój projekt wyświetla statyczne pliki Markdown z lokalnego folderu, sprawdzona biblioteka markdown-it poradzi sobie bez niepotrzebnych komplikacji. Ale jeśli pracujesz ze strumieniem tokenów na żywo z LLM, przenosisz interfejs czatu do sieci lub masz dość walki z opóźnieniami podczas renderowania długich odpowiedzi, biblioteka zdecydowanie zasługuje na miejsce w Twoich zależnościach.
Eliminuje dziesiątki nieoczywistych problemów ze strumieniowaniem i oszczędza mnóstwo czasu na pisanie własnych obejść wokół parserów. Aby szybko zacząć, możesz sprawdzić oficjalny internetowy playground lub wdrożyć środowisko testowe przez StackBlitz.
Powiązane projekty