はじめに
ChatGPT風のチャットUIを作ったことがある人なら、この苦しみを知っているはずです。
AIの応答をリアルタイムでストリーミング表示しようとすると、Markdownが途中で途切れて崩れる。コードブロックが閉じていない状態で表示されて見た目がぐちゃぐちゃ。リストの途中で改行されて変なレイアウトになる。これ、意外と根深い問題なんですよね。
react-markdownを使っていた自分も、ストリーミング中の表示崩れには何度も悩まされました。「完全に受信してからレンダリングすれば良いじゃん」という話もありますが、それだとユーザー体験が悪い。やっぱりリアルタイムで文字が流れてくるあの体験が欲しいわけです。
そんな中で登場したのがStreamdownです。Vercelが開発した、AIストリーミングに特化したMarkdownレンダラーで、公開からわずか4ヶ月でGitHubスター3,400超え。これ、同じ悩みを持っていた人が世界中にいたということなんでしょうね。
Streamdownとは
Streamdownは、react-markdownのドロップイン置き換えとして設計されたReactコンポーネントです。「A drop-in replacement for react-markdown, designed for AI-powered streaming」というのが公式のキャッチコピー。
要するに、AIがMarkdown形式で応答を返してくるシナリオ、特にストリーミングでちょっとずつテキストが流れてくる状況で、崩れることなく綺麗にレンダリングしてくれるライブラリです。
普通のreact-markdownだと、不完全なMarkdownブロック、たとえば閉じていないコードブロックや途中のリストを渡すと表示が崩れます。Streamdownはそこを解決するために設計されているんですよね。
特徴・メリット
1. 不完全なMarkdownをシームレスに処理
これがStreamdownの最大の特徴です。
ストリーミング中は、Markdownの構文が途中で切れている状態が頻発します。たとえば、バッククォート3つで始まるコードブロックが閉じていない、見出しの#の直後で途切れている、リンクの](が来る前に切れている、など。
Streamdownはこうした「未終了ブロック」を適切に解析して、崩れることなく表示してくれます。個人的には、これだけでも乗り換える価値があると思っています。
2. 組み込みのTailwind CSSスタイル
見出し、リスト、コードブロックなど、一般的なMarkdown要素に対してTailwindベースのスタイルが最初から適用されています。
react-markdownだと、コンポーネントをカスタマイズしてスタイルを当てる作業が必要でしたが、Streamdownならそのまま使ってもそこそこ綺麗に表示される。時短になりますね。
3. CJK言語の完全サポート
これ、地味に嬉しいポイントです。
日本語、中国語、韓国語のテキストで、表意文字の句読点を使った強調マーカーが正しく機能します。英語圏のライブラリだと、日本語の処理がおかしくなることがあるんですよね。そこをちゃんと考慮してくれているのは助かります。
4. Mermaid図の描画対応
コードブロック内のMermaid記法を、そのまま図として描画できます。
AIにフローチャートやシーケンス図を生成してもらうケースが増えてきているので、これは実用的な機能です。わざわざ別のライブラリを組み合わせる必要がない。
5. Shikiによるシンタックスハイライト
コードブロックのシンタックスハイライトには、Shikiが使われています。
Prism.jsやhighlight.jsではなくShikiというのが今どき感がありますね。VSCodeと同じトークナイザーを使っているので、ハイライトの精度が高いです。
6. KaTeXで数式レンダリング
LaTeX形式の数式をKaTeXでレンダリングできます。
技術系のAIチャットだと、数式を返してくることも多いので、これが標準で対応しているのは便利です。
7. セキュリティ対策
予期しないオリジンからの画像やリンクに対する、組み込みのセキュリティ対策があります。
AIが生成したMarkdownをそのまま表示するのは、セキュリティ的にリスクがあります。rehype-hardenを使って、安全なレンダリングを実現しているのは安心ポイントですね。
インストール方法
npmでインストールします。
npm i streamdown
Tailwind CSSを使っている場合は、globals.cssに以下を追加する必要があります。
@source "../node_modules/streamdown/dist/*.js";
これで、Streamdownが使っているTailwindクラスがビルドに含まれるようになります。Tailwind v4の新しい記法ですね。
基本的な使い方
最小構成
import { Streamdown } from "streamdown";
export default function ChatMessage({ content }: { content: string }) {
return <Streamdown>{content}</Streamdown>;
}
react-markdownを使っていた人なら、ほぼ同じ感覚で使えます。子要素としてMarkdown文字列を渡すだけ。
ストリーミング表示の実装例
Vercel AI SDKと組み合わせる場合のパターンです。
"use client";
import { useChat } from "ai/react";
import { Streamdown } from "streamdown";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div className="flex flex-col gap-4">
{messages.map((message) => (
<div key={message.id}>
<p className="font-bold">{message.role}:</p>
<Streamdown>{message.content}</Streamdown>
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="メッセージを入力..."
className="border rounded px-4 py-2 w-full"
/>
</form>
</div>
);
}
AIからの応答がリアルタイムでストリーミングされても、Markdownが崩れることなく表示されます。
コードブロックのカスタマイズ
コピーボタンやダウンロード機能は標準で備わっていますが、さらにカスタマイズしたい場合もあるでしょう。
import { Streamdown } from "streamdown";
export default function Page() {
const markdown = `
# サンプルコード
\`\`\`typescript
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
console.log(greet("World"));
\`\`\`
`;
return (
<div className="prose">
<Streamdown>{markdown}</Streamdown>
</div>
);
}
実践的なユースケース
AIチャットボットのUI
これが最も典型的なユースケースです。
"use client";
import { useState, useEffect } from "react";
import { Streamdown } from "streamdown";
interface Message {
role: "user" | "assistant";
content: string;
}
export default function Chatbot() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
const userMessage: Message = { role: "user", content: input };
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
// ストリーミングレスポンスの処理
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ message: input }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let assistantContent = "";
setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
while (reader) {
const { done, value } = await reader.read();
if (done) break;
assistantContent += decoder.decode(value);
setMessages((prev) => {
const newMessages = [...prev];
newMessages[newMessages.length - 1].content = assistantContent;
return newMessages;
});
}
setIsLoading(false);
};
return (
<div className="max-w-2xl mx-auto p-4">
<div className="space-y-4 mb-4">
{messages.map((msg, i) => (
<div
key={i}
className={`p-4 rounded-lg ${
msg.role === "user" ? "bg-blue-100" : "bg-gray-100"
}`}
>
<Streamdown>{msg.content}</Streamdown>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
className="flex-1 border rounded px-4 py-2"
placeholder="質問を入力..."
/>
<button
type="submit"
disabled={isLoading}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
送信
</button>
</form>
</div>
);
}
ドキュメント生成ツール
AIがドキュメントを生成する際にも使えます。
import { Streamdown } from "streamdown";
interface DocPreviewProps {
content: string;
isGenerating: boolean;
}
export function DocPreview({ content, isGenerating }: DocPreviewProps) {
return (
<div className="prose prose-lg max-w-none">
<Streamdown>{content}</Streamdown>
{isGenerating && (
<span className="inline-block w-2 h-4 bg-gray-400 animate-pulse" />
)}
</div>
);
}
技術ブログのプレビュー
Markdownで書いた記事のプレビューにも使えます。Mermaidや数式が含まれていても対応できるので、技術系のコンテンツに向いています。
import { Streamdown } from "streamdown";
export function BlogPreview({ markdown }: { markdown: string }) {
return (
<article className="prose prose-slate max-w-none">
<Streamdown>{markdown}</Streamdown>
</article>
);
}
まとめ
Streamdownは、AIストリーミング時代のMarkdownレンダリングという、ニッチだけど確実に需要のある問題を解決してくれるライブラリです。
個人的に評価しているポイントはこのあたり。
- 不完全なMarkdownでも崩れない
- react-markdownからの移行が簡単
- CJK言語のサポートがしっかりしている
- Mermaid、KaTeX、Shikiが標準装備
- Tailwindとの親和性が高い
正直なところ、ChatGPT風のUIを作ろうとして「ストリーミング中にMarkdownが崩れる」問題にぶつかった人は多いと思います。これまでは自前で対策を書いたり、完全に受信してからレンダリングしたりという妥協が必要でした。
Streamdownはその問題を正面から解決してくれます。Vercel製という安心感もありますし、AI関連のプロダクトを作っているなら、選択肢の一つとして検討する価値は十分にあると思いますね。
まずはnpm i streamdownで試してみてください。