概要
Electronでドキュメントビューアアプリを作った。~/.claude/配下のファイルが動的に追加・削除される環境で、それらを表示するビューア。最初はDocusaurusを使ったが、ファイルの追加・削除で頻繁にエラーが出て不安定だった。Astroに切り替えたらうまく動いた。


ただ、なぜうまくいったのか、何が根本的に違うのかがちゃんとわかっていなかった。それでClaudeに聞き始めたら、バンドラーアーキテクチャの話からAstroのisland、Next.jsのSSR/streamingまで話が広がった。その会話の記録。
Docusaurusでなぜ動的なファイル変更がうまくいかないのか
Takazudo
Electronでドキュメントビューアを作った。~/.claude/の中のファイルが常に追加されたり消えたりする。Docusaurusで表示しようとしたら、ファイル追加・削除のたびにエラーが出て不安定だった。なんで?
Claude
Docusaurusは内部でwebpackを使っている。webpackの特徴は、ビルド時にすべてのファイルを「依存関係グラフ」として結合するところにある。
既存ファイルの編集は、電球の交換みたいなもの。配線はそのまま、中身だけ変わる。HMR(Hot Module Replacement)が効いて、変更は即座に反映される。
でもファイルの追加・削除は違う。部屋の増改築みたいなもので、配線図全体を引き直す必要がある。Docusaurusでは、1つのドキュメント = 1つのルート = 依存関係グラフの1ノードになっている。ドキュメントが追加されると何が起きるかというと、以下の一連の処理が走る。
- ルートの再生成
- サイドバーの再計算
- 仮想モジュールの再構築
- webpackのフルリコンパイル
これはwebpackが壊れているとか、欠陥があるとかいう話ではない。バンドラーのアーキテクチャは「エントリーポイントは安定している」ことを前提にしている。ファイルが頻繁に追加・削除される状況は、完成した建物に毎日増改築を繰り返すようなもの。「一度建てて使う」前提の構造には合わない。
普通のドキュメントサイトの開発なら問題にならない。ファイルの追加・削除はたまにしか起きないし、そのときにフルビルドが走るのは普通のこと。ただ、ファイルが常に変動するビューアアプリでは、このアーキテクチャとの不一致が顕在化する。
これはDocusaurusに限った話ではなく、バンドラーベースのフレームワーク全般に言える構造的な特性。
Astroのアプローチ
Takazudo
Astroだとシンプルだよね。ファイルごとに独立してビルドできるから。監視して、1ページビルドして、配信する。ナビの更新が必要なら別途やる。Astroにはファイル単位のビルドAPIもあるよね?
Claude
方向としてはそう。Astroは各ページを独立にレンダリングする。ページ間の依存関係グラフがない。ファイルが1つ増えても「HTMLが1つ増える」だけで、グラフの再構築は起きない。
ただし、Astroには「1ファイルだけビルド」するAPIはない。astro buildは毎回すべてのページをリビルドする。
でもリビルドが速い。依存関係グラフの再構築がないから、5秒程度で終わる。AstroにはクリーンなプログラマティックAPIがある。
import { build } from "astro";
await build();
Node.jsから簡単に呼べる。これだけ。
軽さの源泉は「インクリメンタルビルド」ではなく「ページが独立しているアーキテクチャ」。全ページをリビルドしているのに速い。それはグラフの再構築が不要だから。
Takazudo
あ、そうなの。全ページビルドなんだ。でもグラフ再構築がないから速い、と。
Astro上でReactを使うとバンドルは発生する?
Takazudo
でもAstroでReactコンポーネント(island)を使ったら、バンドルは発生するよね?
Claude
Astroは内部でViteを使っていて、バンドルのステップはある。ただしスコープが全然違う。
Docusaurus(webpack)の場合は、すべてがバンドラーを通る。全ページ、ルーティング、コンテンツ、UI、全部まとめて1つの大きなSPAバンドルになる。
Astro(Vite)の場合はこう。
- ページのレンダリング --- AstroのSSGパイプラインがHTMLを出力する。バンドラーは関与しない
- React island(
client:load等) --- Vite/Rollupが個別のJSチャンクとしてバンドルする
ただしislandはページ全体のごく一部。ほとんどは静的HTML。
Takazudo
複数のReact islandを置いた場合、Reactランタイム自体はどうなる?islandごとにReactが入る?それとも共有?
Claude
Vite/Rollupがコード分割を自動でやる。
- Reactランタイム --- 共有チャンクとして1回だけ読み込まれる
- 各island --- 自分のコンポーネントコードだけの小さなチャンク
ReactがislandごとにDuplicateされることはない。
例えば3つのislandがあるページだとこうなる。
react-vendor.js ← Reactランタイム(共有、1回だけロード)
island-sidebar.js ← SidebarTreeコンポーネントだけ
island-toc.js ← Tocコンポーネントだけ
island-search.js ← Searchコンポーネントだけ
islandがゼロのページ = JSゼロ。islandが1つのページ = Reactランタイム + そのコンポーネント1つ。同じislandを使う別のページがあっても、既存のJSチャンクが再利用される。新しいバンドルは不要。
ここで重要なのは、コンテンツ・ルーティング・ページ構造はバンドラーを一切通らないということ。バンドラーが処理するのは小さなインタラクティブislandだけ。Docusaurusだと100%がwebpackを通る。Astroだと5-10%(islandだけ)がViteを通る。
Astroが常に最適?
Takazudo
そう聞くとAstroがいつも良く聞こえるけど…
Claude
それは違う。AstroでフルSPAを作ろうとしたら(1つの巨大なReact islandですべてを処理する)、Astroはただの殻。何もしていない。その場合はVite + Reactで直接作ったほうがシンプル。
SSRの話をするなら、Next.jsはAstroよりずっと強力。
- Next.js --- フルのReact SSR、React Server Components、streaming、ISR(Incremental Static Regeneration)、APIルート、middleware
- Astro SSR --- SSRモードはあるが、ISRもstreamingもない。シンプル
それぞれのツールに適した領域がある。
| ツール | 向いている用途 |
|---|---|
| Vite + React | クライアントサイドSPA、ダッシュボード |
| Next.js | フルReact SSR、ECサイト、複雑なデータフェッチ |
| Astro | コンテンツ主体の静的サイトで軽いインタラクティビティ(island) |
| Docusaurus | バージョニング・i18n・検索つきのドキュメントサイト |
Next.jsとサーバープッシュ
Takazudo
Next.jsでiOSみたいなサーバーサイドのプッシュ通知ってできる?
Claude
それは実はフレームワークの機能ではなくて、トランスポート層の話。
- WebSocket --- 双方向、常時接続
- SSE(Server-Sent Events) --- サーバーからクライアントへの一方通行
- Long polling --- フォールバック
どんなSPAでもできる。Next.js固有ではない。
Takazudo
あー、そうか。プッシュ通知はWebSocketの領域で、Next.jsの外側の話か。
Claude
そう。で、Electronならもっとシンプル。IPC(Inter-Process Communication)がある。
// メインプロセス(Node.js)— ファイル変更を検知
mainWindow.webContents.send("file-changed", { path: "skills/new-skill" });
// レンダラー(React)— 受け取る
ipcRenderer.on("file-changed", (event, data) => {
// サイドバー更新、コンテンツリロード等
});
ネットワーク層なし、WebSocketサーバーなし。プロセス間で直接メッセージをやりとり。Electronの強みの一つ。
Next.jsの大量ページ対応とブラックボックス化
Takazudo
Next.jsにはISRがあるよね。数千ページあるサイトで、最初のリクエスト時にページを生成してキャッシュして再検証する。Astroにはこういうのはできない。ただ、Next.jsがどんどん大きなブラックボックスになっていく感じがする…。これは問題?
Claude
ISRは大規模サイト(ニュースサイト、数百万ページのECサイト)にはキラーフィーチャー。最初のリクエストで生成、キャッシュ、N秒後に再検証。全ページを事前ビルドする必要がない。
Astroには基本的なSSRはあるがISR(キャッシュ + 再検証戦略)はない。
「ブラックボックス」の懸念は業界でも広く議論されている。Next.jsは巨大になった。
- App Router
- Server Components
- Server Actions
- Middleware
- Edge Runtime
- ISR
- streaming
キャッシングの挙動があまりに分かりにくくて、v15でデフォルトを変更したぐらい。Server Componentsのメンタルモデルが複雑で、どこがサーバーでどこがクライアントなのかが掴みにくい。Vercelのプラットフォームとの密結合もある。Vercelで動かすのが一番スムーズで、セルフホスティングは大変。
Next.js自身がwebpackからTurbopack(Rustベース)に移行中で、webpackの限界を開発元自身が認識しているという話でもある。
これはトレードオフ。大きなチーム + 複雑なWebアプリ = Next.jsの複雑さは正当化される。コンテンツサイト = 不要なオーバーヘッド。
Next.jsのstreamingとは何か
Takazudo
Next.jsの「streaming」ってなに?
Claude
従来のSSRはこう。
サーバー: ヘッダーレンダリング... サイドバー... メインコンテンツ(重いDBクエリ3秒)... フッター
─── 待ち 待ち 待ち ───
サーバー → ブラウザ: [完成したHTML一括送信] (3秒後)
サーバーがページ全体をレンダリングし終えてから、完成したHTMLを一括送信する。ユーザーは最も遅い部分の処理が終わるまで何も見えない。
streaming SSRはこう。
サーバー → ブラウザ: [ヘッダー + サイドバー + ローディングスピナー] (50ms)
サーバー → ブラウザ: [メインコンテンツがスピナーを置き換え] (3秒後)
サーバー → ブラウザ: [フッター] (3.1秒後)
ブラウザは即座にコンテンツを表示し始める。遅い部分は準備できた順に差し込まれる。
HTTPのchunked transfer encodingを使う。1つのレスポンスを開きっぱなしにしてチャンクを送る仕組み。
Reactの<Suspense>で制御する。
export default function Dashboard() {
return (
<div>
<Header /> {/* 即座に送信 */}
<Suspense fallback={<Spinner />}>
<SlowDataPanel /> {/* DBクエリが完了した時点でストリーミング */}
</Suspense>
</div>
);
}
ページの部分ごとにデータ取得の速度が違う場合に有効。ニュースサイトで言えば、ヘッダーは速い、パーソナライズされたレコメンドは遅い。レコメンドだけ後から流し込む。
Astroやccdocにはこれは不要。事前ビルドした静的HTMLを配信しているだけだから。サーバーサイドのデータフェッチがない。
Server Componentsとstreamingの違い
Takazudo
それがServer Componentsってやつ?
Claude
近いけど、2つは別の概念。
- React Server Components(RSC) --- コードがどこで実行されるかの話。サーバー側でのみ実行される。クライアントにJSが送られない。DB、ファイルシステムに直接アクセスできる
- Streaming --- レンダリング結果がどう配信されるかの話。一括ではなくチャンクで順次送る
組み合わせるとうまくハマる。
// Server Component(サーバーでのみ実行、JSはクライアントに送られない)
async function UserPosts() {
const posts = await db.query("SELECT ...");
return (
<ul>
{posts.map((p) => (
<li>{p.title}</li>
))}
</ul>
);
}
// ページ:streaming + server componentの組み合わせ
export default function Page() {
return (
<div>
<Header /> {/* server component、速い */}
<Suspense fallback={<Spinner />}>
<UserPosts /> {/* server component、DBが返ったらストリーミング */}
</Suspense>
</div>
);
}
| 概念 | 答える問い |
|---|---|
| Server Components | コードはどこで実行される?(サーバーのみ、JSをクライアントに送らない) |
| Streaming | 結果はどう配信される?(一括ではなくチャンクで順次) |
実は、AstroのislandアーキテクチャとReact Server Componentsには哲学的な類似性がある。Astroコンポーネントは「サーバーのみ」で、HTMLにレンダリングされてJSは送られない。React islandが「クライアントコンポーネント」。AstroはReact Server Componentsが登場する前からこのアイデアを持っていた。
SuspenseはサーバーとWebSocketで通信する?
Takazudo
Suspenseがサーバーサイドのリアクトシステムに追加のデータを送る仕組みがある?それでVercelが最適ということ?
Claude
いや、もっとシンプル。1つのHTTPレスポンスを開きっぱなしにするだけ。サーバーがHTMLチャンクとインラインの<script>タグをそのレスポンスストリームにpush。WebSocketなし、追加リクエストなし。
流れはこう。
ブラウザ: GET /dashboard
サーバー: HTTP 200 (Transfer-Encoding: chunked)
↓ [チャンク1: ページシェル + <Spinner/>] ← 即座に送信
... サーバーはまだ重いクエリを処理中 ...
↓ [チャンク2: <script>がSpinnerを実コンテンツに差し替え] ← 3秒後
↓ [接続クローズ]
1リクエスト、1レスポンス。サーバーが接続を開きっぱなしにしてピースを送るだけ。
Vercelの利点はSuspenseとは別の話。インフラスタック全体の話。
- Edge Runtime --- 世界中のCDNノードでServer Componentsを実行(ユーザーに近い = 速い)
- ISRキャッシュ --- 再検証の仕組みがNext.jsと深く統合
- Serverless --- APIルート/Server Componentsが自動スケール
- ビルドシステム --- Next.js用に最適化
そして核心。VercelはNext.jsチームとReactコアチームメンバーの両方を雇用している。機能が共同設計される。
「ブラックボックス」の懸念はここにもつながる。機能がまずVercel向けに設計されて、セルフホスティングは二の次になる傾向がある。
まとめ
Docusaurusがうまくいかなかった理由から始まった会話だったが、アーキテクチャの根本的な違いの理解まで広がった。
「どれが最良か」ではなく「何に向いているか」で選ぶ話。バンドラーアーキテクチャ、ページ独立ビルド、フルSSRフレームワーク、それぞれに向き不向きがある。背景を理解することで、自分が何をやっているのかが見えてくる。
余談
なおそのビューアーは以下の /doc/ にあります。~/.claude/doc/に突っ込んでビルドすると動きます。たぶん……。