🏎️

ReactのrenderToReadableStreamをHono v3.7で使う

2023/09/28に公開

みなさん、Hono 使ってますか?
先日 Cloudflare Meetup というイベントに参加してきました。
そこで Hono 作者の @yusukebe さんと話す機会があり、「Hono の Contribute をしないか」とお誘いをいただいたので、微力ながら参戦してきました。


c.stream()c.streamText() というAPI

さて、先日 Hono v3.7 がリリースされましたね。

https://github.com/honojs/hono/releases/tag/v3.7.0

私はこのリリースの 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をプロキシする

これができると、サービスに 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;

hono-react-stream

DelayComponent は 3 秒後に Finished! という文字列を表示するコンポーネントです。
renderToReadableStream を使うことで、DelayComponent のレンダリングが終わるまで待たずに、Loading 3 sec... という文字列を先に表示できます。

それだけです... Hydration まで書くのはめんどくさくてやってません、ごめんなさい。

追記: Yusukebe さんが以前、c.stream() がなかった頃の Hono で同じことをする記事を書いていたらしい
https://zenn.dev/yusukebe/articles/41becfd057c416

今後: Honoの次リリースでSSEが使えるようになる予定

Hono v3.8 では、SSE helper が使えるようになる予定です。
通常の Stream Response は HTTP/2 でのみ使えるのですが、SSE helper を使うことで HTTP/1.1 でも Stream Response を使うことができます。

こちらは現在 Approve が出ているのでもうすぐ lambda や bun などの HTTP/2 がサポートされていないランタイムでもストリームが使えるようになりそう!

https://github.com/honojs/hono/pull/1504

ということでどちらかというと Hono v3.7 の紹介記事みたくなってしまいましたがここまで読んでいただきありがとうございました🙇‍♂️

おまけ

書けハラされました

書けハラ

GitHubで編集を提案

Discussion