RORK LABEN
PLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっていますPLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっています
記事一覧/開発ツール
開発ツール/2026-04-13上級

Rork アプリで LLM ストリーミングを実装する — ChatGPT 風リアルタイム AI 応答 UI を Expo × SSE で本番構築する

React Native / Expo で LLM ストリーミング(SSE)を本番実装した記録。Anthropic・OpenAI・Gemini の抽象化、AbortController とリトライ、Cloudflare Workers プロキシ、会話履歴の圧縮、課金ゼロで検証するモック SSE サーバーまで動作コード付き。

streamingSSELLM3AI30Anthropic4OpenAI5React Native234Expo194Cloudflare Workers24Rork548

プレミアム記事

送信ボタンを押してから 12 秒、画面の真ん中でスピナーだけが回り続けていました。そのあいだ何も起きない。ようやく回答が出た瞬間に、テスターの指はもうホーム画面に伸びていた——自分のアプリで AI チャットを最初に組んだとき、私が最初に直面したのはこの光景でした。

モデルの応答が遅いわけではありません。生成が終わるまで待ってから一括で返す、という設計そのものが待ち時間を「無」にしてしまっていた。トークンが生成された端から流れてくれば、同じ 12 秒でも読者は読み始められます。

そこで React Native に SSE(Server-Sent Events)を持ち込もうとして、二度目の壁にぶつかりました。ブラウザで当たり前に使う EventSource が、React Native のランタイムには存在しません。fetch() からストリームを読む手順も、AbortController の噛ませ方も、Web の記事をそのまま持ってくると必ずどこかで折れます。

このページは、その折れた箇所をひとつずつ埋めていった記録です。Anthropic・OpenAI・Gemini の 3 社を同じインターフェースで扱う抽象化、API キーを端末に置かないための Cloudflare Workers プロキシ、そして課金を発生させずにストリーミングを検証する方法まで、実際に本番で動いているコードを載せています。

なぜ LLM ストリーミングが UX の分水嶺になるのか

LLM API には 2 つの呼び出し方があります。一括レスポンス(blocking)ストリーミング(streaming) です。

一括レスポンスは単純です。リクエストを投げ、モデルが全文生成を終えてから JSON が返ってきます。実装が簡単な代わりに、長い回答では待ち時間がそのまま無音の時間になります。Claude Sonnet クラスのモデルに 800 字程度の説明を書かせると、私の手元では 15〜20 秒に達することが珍しくありませんでした。

この無音が何を引き起こすかは、数字より先にテスターの表情で分かります。5 秒を超えたあたりで一度画面から目を離し、10 秒を超えると別のアプリに切り替える。戻ってきたときには何を尋ねたのかを忘れている。離脱率を計測する以前の問題として、会話が続かなくなります。

ストリーミングは、LLM がトークンを生成するたびにその断片を逐次送信します。ユーザーには数百ミリ秒後から文字が流れ始め、全体の生成時間が同じでも「速い」と感じられます。待つ側から読む側へ、姿勢が変わる。ここが分水嶺です。

もう一つ、見落とされがちな利点がコストの制御です。ストリーミングを途中でキャンセルすれば、そこまでに生成されたトークン分しか課金されません(Anthropic・OpenAI ともに途中キャンセルに対応しています)。「もう分かったので止めたい」という場面で停止ボタンを押してもらえる状態にしておくと、想定より長い回答が延々と生成され続ける事故を防げます。私のアプリでは、この停止ボタンを付けた月の請求額が明確に下がりました。

React Native で SSE を扱う際の根本的な違い

Web ブラウザでは EventSource API を使って SSE を受信できます。しかし React Native では EventSource は存在しない。これを知らずに new EventSource(url) を書いてビルドが通り、実機で真っ白になるパターンは定番の落とし穴です。

ストリーミングの実体は fetch() から ReadableStream を読むことです。ただしここに、私が半日を溶かした落とし穴がもうひとつ埋まっていました。

React Native のグローバル fetch は、response.body を返しません。 RN の fetch は XMLHttpRequest の上に載せたポリフィルで、ストリーム対応は仕様として未実装のままです(facebook/react-native#27741 が長く開いたままなのが実情です)。Expo SDK を上げれば直る、という種類の問題ではありません。

使うのは、Expo SDK 52 で追加された expo/fetch です。名前付きインポートで取り込む別実装で、ネイティブ側にストリーム対応の fetch を持っています。グローバルの fetch を置き換えるものではないため、インポート文を書き忘れると静かに旧実装へ落ちます。ここが本当に見つけにくい。

// ❌ ブラウザでは動くが RN に EventSource は存在しない
const es = new EventSource('https://api.anthropic.com/v1/messages');
 
// ❌ グローバル fetch — 通信自体は成功するのに body が取れない
const bad = await fetch(url, { method: 'POST', body });
bad.body; // → undefined(RN のポリフィル実装)
 
// ✅ Expo SDK 52 以降。名前付きインポートが必須
import { fetch } from 'expo/fetch';
 
const response = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'anthropic-version': '2023-06-01',
    'x-api-key': 'YOUR_ANTHROPIC_API_KEY', // 本番ではプロキシ経由で渡す(後述)
  },
  body: JSON.stringify({ stream: true, ...params }),
});
 
// response.body は ReadableStream
const reader = response.body?.getReader();
const decoder = new TextDecoder();
 
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value, { stream: true });
  // chunk を解析して UI を更新
}

response.bodyundefined のまま返ってきたら、原因はほぼ次の3つのどれかです。

症状原因対処
body が undefined。エラーは出ないexpo/fetch をインポートしていないimport { fetch } from 'expo/fetch'; を追加する
インポートしても undefinedExpo SDK 51 以前SDK 52 以上へ上げる。上げられない場合は react-native-sse などのライブラリに退避する
Web では動くが実機だけ落ちるEXPO_PUBLIC_USE_RN_FETCH=1 が効いているこの環境変数はグローバルを RN 実装へ戻すためのもの。名前付きインポート側は影響を受けないため、まずインポート経路を確認する

Rork が生成するプロジェクトは新しい SDK を使いますが、expo/fetch を明示的に呼ぶかどうかは生成されたコード次第です。ストリーミングを入れるときは、まずインポート文を目で確認してください。

SSE フォーマットの解析

Anthropic の streaming API は以下の形式でチャンクを送信します。

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"こんにちは"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"、今日は"}}

一つの read() 呼び出しで複数の SSE イベントが含まれることがあります。また、一つのイベントが複数の read() に跨がって届くこともあります。これを正しく処理しないと、JSON parse エラーが散発的に発生します。

OpenAI の streaming 形式は少し異なります。

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"こんにちは"},"index":0}]}

data: [DONE]

それぞれのパーサーを画面側に直接書いてしまうと、プロバイダーを増やすたびに UI が壊れます。抽象化レイヤーを一枚挟むことを強くおすすめします(後述)。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
SSE の fetch 実装で詰まっていた人が、React Native で LLM ストリーミング接続を今日から本番環境で動かせる
AbortController・エラーリトライ・コスト最適化の3セットが揃った、コピペで使える実装コードを手に入れられる
Anthropic/OpenAI/Gemini の3社 API に対応した抽象化レイヤーの設計パターンを習得し、プロバイダー切り替えをアプリ再起動なしで実現できる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-08-22
一括置換は全ファイル成功しました。壊れたのは、消さなかった行のほうです
生成コードに一括置換をかけると、一致した行ではなく隣の行が壊れます。運用中のプロジェクトで実際に起きた破損と、置換の不変条件を検査する20行のガードスクリプトをまとめました。
開発ツール2026-07-30
Renovate が触ってよい依存と、触ってはいけない依存 — Expo SDK 57 の123パッケージを機械的に切り分ける
Rork 生成アプリに依存の自動更新を入れると、Expo が版を管理している123パッケージまで巻き込まれます。2026年7月30日時点の実測では、うち6件が npm の最新版へメジャー更新される差でした。除外リストを手で書かず SDK から機械生成する手順をまとめます。
開発ツール2026-07-28
prebuild --clean で消える手入れを、上げる前に数える — Expo SDK 57 移行の棚卸し
Expo SDK 57 への移行前に、ios/ へ加えた手入れが prebuild の再生成でいくつ失われるかを数える手順をまとめました。素の差分649行のうち意味を持つのは3行。ノイズを正規化して実質的な変更だけを抽出するスクリプトと、config plugin への移し替えを実測つきで共有します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →