はじめに
Chrome 151 で Request・Response・Blob に textStream() メソッドが追加されました。
これまでバイトストリームをテキストとして読み取るには TextDecoderStream を経由するパイプ処理が必要でしたが、
textStream() を使うとその手順をそのままショートカットできます。
LLM のストリーミングレスポンスや大きなテキストファイルの逐次処理など、
テキストをストリームで扱う場面で役立ちます。
従来の書き方との比較
従来:TextDecoderStream を経由する
const response = await fetch('/api/stream');
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // デコード済みテキスト
}
response.body はバイト列(Uint8Array)を流す ReadableStream のため
、テキストとして読み取るには TextDecoderStream でデコードする必要がありました。
Chrome 151 以降:textStream() を使う
const response = await fetch('/api/stream');
const reader = response.textStream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // デコード済みテキスト
}
textStream() は「バイトストリームを TextDecoderStream にパイプしたもの」と等価な ReadableStream<string> を返します。
pipeThrough(new TextDecoderStream()) がそのまま省略できます。
for await...of と組み合わせる
ReadableStream は非同期イテラブルなので、for await...of と組み合わせるとより簡潔に書けます。
const response = await fetch('/api/stream');
for await (const chunk of response.textStream()) {
process(chunk);
}
実用例:LLM のストリーミングレスポンスを画面に表示
チャット UI などで LLM のレスポンスをリアルタイムに表示するユースケースに特に相性が良いです。
async function streamChat(message) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
const output = document.getElementById('output');
for await (const chunk of response.textStream()) {
output.textContent += chunk;
}
}
従来は TextDecoderStream と getReader() を組み合わせるボイラープレートが必要だったものが、textStream() + for await...of で完結します。
Blob でも使える
Response だけでなく Blob にも同じ textStream() が追加されています。
const blob = new Blob(
['1行目\n2行目\n3行目\n'],
{ type: 'text/plain' }
);
for await (const chunk of blob.textStream()) {
console.log(chunk);
}
ファイル選択(<input type="file">)で取得した File オブジェクトは Blob のサブクラスなので、そのまま textStream() が使えます。
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
for await (const chunk of file.textStream()) {
processLine(chunk);
}
});
まとめ
textStream() は小さな追加ですが、テキストストリームを扱うたびに書いていた pipeThrough(new TextDecoderStream()) のボイラープレートを一掃できます。
LLM レスポンスのストリーミング表示や大きなテキストファイルの逐次処理など、ストリームでテキストを扱う機会が増えている今、覚えておきたい API のひとつです。
参考
最後まで読んでくださってありがとうございます!
普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。