ReactのrenderToReadableStreamをHono v3.7で使う
みなさん、Hono 使ってますか?
先日 Cloudflare Meetup というイベントに参加してきました。
そこで Hono 作者の @yusukebe さんと話す機会があり、「Hono の Contribute をしないか」とお誘いをいただいたので、微力ながら参戦してきました。
c.stream()
と c.streamText()
というAPI
さて、先日 Hono v3.7 がリリースされましたね。
私はこのリリースの c.stream()
と c.streamText()
という API の開発を担当させていただきました。
これは Web Standard の ReadableStream という API のラッパーに当たります。
元々原案を @geelen さんが作ってくれていて、それを元に実装を進めていきました。
たとえばこんなコードが動きます
カウントアップ
app.get("/", (c) => {
return c.streamText(async (stream) => {
for (let i = 0; i < 10; i++) {
await stream.writeln(`Hello ${i}`);
await stream.sleep(1000);
}
});
});
「一定時間コネクションを張り続けて受信する」どこかでみたことある挙動ですよね。
そうです、ChatGPT でよくみるアレです。
ChatGPTをプロキシする
app.post('/api', async (c) => {
const body = await c.req.json<{ message: string }>()
const openai = new OpenAI({ apiKey: c.env.OPENAI_API_KEY })
const chatStream = await openai.chat.completions.create({
messages: PROMPT(body.message),
model: 'gpt-3.5-turbo',
stream: true
})
return c.streamText(async (stream) => {
for await (const message of chatStream) {
await stream.write(message.choices[0]?.delta.content ?? '')
}
})
})
これができると、サービスに ChatGPT API を組み込むときに
-
OPENAI_API_KEY
をサーバー側に隠蔽できる - レート制限がかけやすくなる
- リクエストをキャッシュしやすくなる
- LangChain を Edge で扱える
などなどさまざまなメリットがあります。
昨日(9/27)には Cloudflare AI というサービスが発表されるなど、エッジでの LLM ユースケースが盛り上がってきているので、とても良いタイミングでリリースできたなと思います。
renderToReadableStream
をHono v3.7で使う
本題: React v18 では、Suspenseという機能が追加されましたね。
React では今まで renderToString
という関数を用いて SSR を実現していたのですが、新たに renderToReadableStream
という関数が追加されました。
React Tree を ReadableStream として扱うことでより効率的にレンダリングできるようになりました。
一部の読み込みに時間がかかるコンポーネントを非同期にレンダリングすることで、それ以外のコンポーネントのレンダリングを先にしてしまえるというわけです。
renderToReadableStream
を使ってみる
全体のコードは こちら にあります。
import { Hono } from "hono";
import { renderToReadableStream } from "react-dom/server";
import { Suspense } from "react";
let finished = false;
const DelayComponent = () => {
if (finished) {
finished = false;
return <div>Finished!</div>;
}
throw new Promise((resolve) => {
return setTimeout(() => {
finished = true;
resolve(true);
}, 3000);
});
};
const App = () => (
<Suspense fallback={<div>Loading 3 sec...</div>}>
<DelayComponent />
</Suspense>
);
const app = new Hono();
app.get("/", async (c) => {
const stream = await renderToReadableStream(<App />);
return c.stream((s) => s.pipe(stream));
});
export default app;
DelayComponent
は 3 秒後に Finished!
という文字列を表示するコンポーネントです。
renderToReadableStream
を使うことで、DelayComponent
のレンダリングが終わるまで待たずに、Loading 3 sec...
という文字列を先に表示できます。
それだけです... Hydration まで書くのはめんどくさくてやってません、ごめんなさい。
追記: Yusukebe さんが以前、c.stream()
がなかった頃の Hono で同じことをする記事を書いていたらしい
今後: Honoの次リリースでSSEが使えるようになる予定
Hono v3.8 では、SSE helper が使えるようになる予定です。
通常の Stream Response は HTTP/2 でのみ使えるのですが、SSE helper を使うことで HTTP/1.1 でも Stream Response を使うことができます。
こちらは現在 Approve が出ているのでもうすぐ lambda や bun などの HTTP/2 がサポートされていないランタイムでもストリームが使えるようになりそう!
ということでどちらかというと Hono v3.7 の紹介記事みたくなってしまいましたがここまで読んでいただきありがとうございました🙇♂️
おまけ
書けハラされました
Discussion