zudo-paper

webpackとかAstroとかNext.jsとかよく分かってない人がClaudeに教えてもらったログ

Author: Takazudo | 作成: 2026/03/11

概要

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

ccdoc のスキルページ表示。サイドバーにスキル一覧、メインにドキュメント、右にTOC

ccdoc の Diff ビュー。コミット履歴と差分表示

ただ、なぜうまくいったのか、何が根本的に違うのかがちゃんとわかっていなかった。それで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/に突っ込んでビルドすると動きます。たぶん……。

https://github.com/Takazudo/claude-resources