メインコンテンツへスキップ
#Web制作 約8分で読めます

Astro の HTML ストリーミングでレンダリング速度を改善する実装パターン

Astro の SSR ストリーミングを活かしてページ表示を速くする具体的なリファクタリング手法と、experimentalDisableStreaming の使いどころを解説する

先に結論:「JavaScript Streaming」という機能は存在しない

このキーワードで検索してきた人に最初に伝えたいことがある。2026年7月時点で、Astro に「Experimental JavaScript Streaming」という名前の機能は存在しない。Astro が持っているのは HTML ストリーミング——SSR 時にチャンク転送エンコーディングでコンポーネントを逐次送信する仕組みだ。これはデフォルトで有効で、experimental フラグで制御するのは「無効化」の方。

紛らわしい情報が出回っているが、公式ドキュメントとリリースノートを読む限り事実はこうなっている。その上で、このストリーミングを最大限活かしてレンダリング速度を改善する実装パターンを、自分のブログで実際に試した結果とあわせて書く。

ストリーミングの仕組みと「await の置き場所」問題

Astro の SSR では、ページの frontmatter で await した処理がすべて完了するまで、ブラウザには1バイトも送信されない。つまりこう書くと:

---
// src/pages/dashboard.astro
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const notifications = await fetchNotifications(user.id);
---
<html>
  <body>
    <Header user={user} />
    <PostList posts={posts} />
    <Sidebar notifications={notifications} />
  </body>
</html>

3つの fetch が全部終わるまでブラウザは白い画面を見続ける。合計で 800ms かかるなら、TTFB も 800ms 以上になる。

解決策は単純で、await をページから追い出して各コンポーネントに移す。

---
// src/components/PostList.astro
const user = await fetchUser();
const posts = await fetchPosts(user.id);
---
<section>
  {posts.map(post => <article>{post.title}</article>)}
</section>

ページ側は await を持たないので、<html><head><Header> がすぐにブラウザへストリームされる。PostList のデータが揃ったらそこが追加で送られ、Sidebar も同様。ユーザーはヘッダーとレイアウトを即座に目にできる。

自分のブログでダッシュボードページをこのパターンに書き換えたとき、体感で TTFB がだいたい半分くらいになった。DevTools の Waterfall で見ると、最初のチャンクが返ってくるタイミングが明らかに早い。

Promise を直接テンプレートに渡すパターン

コンポーネントを分割するほどでもない場合、await せずに Promise をそのままテンプレートに書く手もある。

---
const namePromise = fetchName(); // await しない
const factPromise = fetchFact(); // await しない
---
<h1>Dashboard</h1>
{namePromise}
<p>Loading fact...</p>
{factPromise}

await なしで Promise を式に置くと、Astro はその箇所だけ解決を待ち、手前の HTML は先に送信する。つまり <h1>Dashboard</h1> は即座に表示され、名前のデータが来たら差し込まれる。

ただしこのパターンには注意点がある。Promise より後ろに書いたマークアップも、その Promise が解決するまでブロックされる。並列に解決させたいなら、依存関係のない Promise は並べる順番を意識するか、別コンポーネントに切り出す方が確実。

experimentalDisableStreaming の使いどころ

Astro 5.2 で @astrojs/react に、5.11 で @astrojs/node アダプターに experimentalDisableStreaming オプションが追加された。名前の通り、ストリーミングを止めるためのフラグ。

// astro.config.mjs — React のストリーミングを止める例
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [
    react({ experimentalDisableStreaming: true })
  ]
});
// astro.config.mjs — Node アダプターのストリーミングを止める例
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  adapter: node({
    mode: 'standalone',
    experimentalDisableStreaming: true
  })
});

使う場面は限られる。自分が実際に必要になったのは、Emotion を使った React コンポーネントをアイランドに入れたとき。ストリーミング中にスタイルの挿入タイミングがずれて、一瞬スタイルなしの状態がチラつく。experimentalDisableStreaming: true で React 側だけ止めたら解消した。

Node アダプター側を止めるケースは、CDN がストリーミングレスポンスをキャッシュできないホスティング環境くらいだと思う。パフォーマンス的にはデメリットしかないので、基本は触らなくていい。

ストリーミングのハマりどころ

実装していて地味に痛かったのが、コンポーネント内でエラーが起きたときのステータスコード問題

Astro のストリーミングでは、レスポンスのステータスコードはページの frontmatter 実行時に決まる。コンポーネントの frontmatter でエラーが発生しても、もう HTTP 200 でヘッダーは送信済み。結果として「200 OK だけど途中で Internal Server Error のテキストが出る」という状態になる。

つまり Sentry のようなエラー追跡を入れるなら、@sentry/astro v9.12.0 以降を使う必要がある(それ以前はストリーミング中のコンポーネントエラーを捕捉できなかった)。

もう1つ。ストリーミング中はコンポーネントから return new Response() でリダイレクトを返すことができない。認証チェックのような「条件を満たさなければリダイレクト」はページの frontmatter でやるしかない。この制約を知らずにミドルウェアではなくコンポーネント内に認証ロジックを書いて、しばらく原因不明のエラーに悩まされた。

実装のチェックリスト

ストリーミングを活かすためにやることは実はシンプルで、要は「ページ frontmatter で重い await をしない」に尽きる。

  1. ページの frontmatter に await がないか確認する
  2. あれば、そのデータを使うコンポーネントに移す
  3. 依存関係のないフェッチは別コンポーネントに分けて並列実行させる
  4. CSS-in-JS(Emotion, styled-components)を使う React アイランドがあれば experimentalDisableStreaming: true を検討
  5. エラーハンドリングはページ frontmatter かミドルウェアに寄せる

大げさなアーキテクチャ変更ではない。await の位置を変えるだけで、ユーザーが最初のコンテンツを目にするタイミングが早くなる。それだけのことだが、効果は体感できるレベルで出る。

参考リンク

#Astro #ストリーミング #SSR #パフォーマンス最適化 #TTFB
シェア