フレームワークとは主張の束である
現代における最高の web application framework を、第一原理から定義する試み — web アプリの定義から導いた 8 つの問題、それぞれに「覆す条件」を付けた 9 つの設計判断、そして誰もほぼ出荷していない 7 つのゲート。
問いをまともに立てる
「最高の web フレームワークは何か」は、たいてい代理戦争である。人が挙げる答えは自分がすでに使っているフレームワークで、議論はサンクコストの弁護になる。ここでは逆をやってみたい。既存の何かに合致するかを確かめる前に、最高の web application framework を定義する。答えが自分のスタックに都合よく出るかどうかも気にしない。
そのためにはまず「最高」という語を分解する必要がある。この語は複数の問いを束ねているからだ:
- 天井 — そのフレームワークで到達できるアプリの質の上限(レイテンシ・オフライン・リアルタイム)
- 床 — 平均的なチームが特に頑張らなくても保証される下限(既定で正しい・安全・速い)
- 検査可能性 — フレームワークが立てる主張のうち、機械が検査しているものの割合
3 つ目がこの文章全体の背骨なので、定義として先に書いてしまう:
フレームワークとはアプリケーションについての主張の束であり、最高のフレームワークとは、最も強い主張を、それを検査するゲートと対にして出荷するものである。
現行世代の主張は、ほぼすべてが宣言であって検査ではない。「キャッシュは revalidateTag で無効化される」— タグを書き忘れた読みは古いデータを黙って返し続け、誰にも何も言われない。「サーバとクライアントは同じ木を描く」— React は開発時に警告し、本番では黙って直す。オラクルは揃っているのに、検査は一度も作られなかった。
web アプリケーションとは何か
定義は、そこから列挙できるときに生産的になる。私の定義はこうだ:
web アプリケーションとは、作者が制御しない実行環境(ブラウザ)で UI が動き、作者が制御する環境に記録の本体があり、信頼できないネットワークがその間にあり、複数の主体が同時に使い、使われている最中に作り変えられ続けるプログラムである。
この文の各節から、問題がちょうど 1 つずつ出る。ここは手を抜いてはいけない部分で、既存フレームワークの機能一覧から出発すると、誰も答えていない問題は最初から視野に入らない。
| # | 問題 | 定義のどの節から出るか |
|---|---|---|
| P-1 | 状態とその写し | 「記録の本体」— すべてのキャッシュとレプリカの一貫性・無効化 |
| P-2 | 境界 | 「信頼できないネットワーク」— 遅延・部分故障・敵対的入力・両端の版差 |
| P-3 | 主体 | 「複数の主体」— すべての効果は誰かとして実行される |
| P-4 | 表示と更新 | 「UI が動く」— 初回描画・更新の粒度・ストリーミング |
| P-5 | 配布 | 「作者が制御しない実行環境」— コード自体が毎回ネットワーク越しに届き、全部は届かないことがある |
| P-6 | 進化 | 「作り変えられ続ける」— スキーマ移行・ローリングデプロイ・版スキュー・インストールベース |
| P-7 | 時間と並行 | 「同時に使う」— 楽観更新・競合・リアルタイム |
| P-8 | 検査可能性 | (メタ) P-1〜P-7 への答えが主張で終わっていないか |
2026 年を「答案」として読む
フレームワークの各系統は、この 8 問への答案である。たいていの欄は空白のままだ:
| 系 | 最も良い答え | 空白のままの欄 |
|---|---|---|
| Next.js (RSC) | P-4 のストリーミングは現状最良。P-1 は半分 | P-2(境界が関数の衣装を着ている)・P-3(認可が ambient)・P-6・P-8 |
| React Router / Remix | P-5(思想としての progressive enhancement)・P-2 の明示性(loader/action) | P-1・P-6 — 思想はあるが検査は無い |
| TanStack Start | P-2 の型付け(端から端) | 型が故障と版を語らない — T の衣装を着た Remote<T> |
| SvelteKit / SolidStart | P-4 の更新粒度(シグナル) | P-1・P-6 |
| Qwik | P-4 の起動(resumability — 同じ仕事を二度しない) | 生態系: 公開 Web での検出は 672 サイト(2026-07) |
| Astro | P-5(zero JS 既定・islands) | アプリケーションより文書向き |
| HTMX / Hotwire | 「分割しない」ことで P-1・P-2 を消す | P-7 の楽観 UI・オフライン・レイテンシの床 |
| Phoenix LiveView | P-7(server push・diff 配信)+ 分割しない | 接続ごとのサーバ状態の費用・オフライン不能 |
| Rails / Laravel / Django | P-6 のうちデータ移行はどこよりも成熟。慣習という床 | P-2・P-4 は一世代前のまま |
| Convex | P-1・P-7(transactional reactive queries) | P-5。DB がフレームワークの一部 |
| Zero / Electric / PowerSync | P-1・P-7 の最前線(query-driven replication。Zero は 2026-06 に 1.0) | P-3 が最難。クライアント常駐スキーマで P-6 も難化 |
| Lamdera | P-6 に型検査された移行で答える唯一の存在。P-2 も 1 言語で型が通る | 規模と生態系 — ただし先例としての価値には無関係 |
| Meteor(歴史) | 下の D-3 と同じ主張を 2012 年に立てた | 実装(oplog tailing)がスケールで死んだ。主張が誤りだったのではなく、スケールの検査なしで出荷された |
そして、ほぼ誰も答えていない問題が 3 つある。
P-6、版スキュー。 デプロイした瞬間、古い HTML を持ったクライアントが新しいサーバを呼ぶ。これは全アプリで毎デプロイ必ず起きるのに、境界に版が無い。今日の実質的な答えは Vercel の Skew Protection だけ — 有料プランのインフラ機能であって、どのフレームワークの意味論にも入っていない。データ側の半分に答えているのは Lamdera だけで、永続化された型を変えるとコンパイラが移行関数を要求する。
P-3、主体。 認可が middleware や context に ambient に置かれている — 経路の属性になっているが、必要なのは効果の属性であることだ。CVE-2025-29927 ではヘッダ 1 本で認可層を丸ごと素通りできた。CVE-2025-66478 — RSC のワイヤプロトコルにおける CVSS 10.0 の直列化復元 RCE — は、境界を敵対的入力のパーサとして扱わなかったときに何が起きるかの実例である。
P-8、検査。 サーバとクライアントの描画一致・無効化の健全性・認可の被覆 — どれもオラクルが手に入るのに、どれもゲートとして出荷されていない。
9 つの判断
ここからが定義の本体である。9 つの判断に、理由と、重要なこととして「何が観測されたらこの判断を捨てるか」を付ける。反証条件を言えない定義は、定義ではなく気分だ。
D-1. 配置は「アプリの選択」ではなく「機能ごとの宣言」
今日、状態をどこで持つか — 全部サーバ(LiveView)・分割(RSC)・クライアント常駐(local-first) — は、フレームワークを選んだ瞬間に全機能へ一括で決まる。だが実際のアプリは混合である。設定画面は server-only で足り、エディタは local-first が要り、決済は server-only であるべきだ。最高のフレームワークは server-only ↔ streamed ↔ interactive ↔ local-first のスペクトラムを機能単位の宣言で張り、使わない配置のコードは出力に一切現れない。「分割しない」派はスペクトラムの端の第一級の点として吸収される。対立するアーキテクチャではない。
覆す条件: 配置宣言が実際には安定しない — 同じ機能が実行時の文脈によって別の配置を要求する — ことが多数派だと実測されたら。
D-2. 境界は関数ではなくプロトコル
server function は境界を関数呼び出しとして売る。その衣装が隠しているものは 4 つある: 部分故障・再試行・両端の版差・敵対的入力。境界は (1) 名前と版を持ち、(2) スキーマを持ち(直列化可能性は型で判定される)、(3) 故障が型に出て — T ではなく Remote<T> — 冪等性を宣言して再試行の意味論をそこから導出し、(4) 互換性ゲート(後述 G-2)を通る。糖衣としての関数構文は良い。嘘をつく型は駄目だ。
覆す条件: 型に出した故障の儀式コストが、捕まえた故障バグの率に見合わないと実測されたら。そのときは既定を「暗黙の境界 + 一律の再試行意味論」に倒す。
D-3. 読みは購読であり、無効化は手で書かせず導出する
人間の注釈によるキャッシュ無効化 — revalidateTag・staleTime — は検査の無い主張である。忘れられたタグは古いデータを黙って返す。最高のフレームワークでは読みは既定で生きているクエリになる: どの書き込みがどのクエリに影響するかをシステムが導出して更新する。導出できないクエリには「腐ってよい」と明示的に宣言させる。既定の向きが逆転する — 腐る読みこそが印の付いた特別な場合になる。
Meteor が 2012 年にまったく同じ主張を立てて、oplog tailing の上で死んだ。だが前提が変わった。logical replication と CDC はいまや DB 側の標準装備で、query-driven replication(Zero)と transactional reactive queries(Convex)は本番実装として存在する。
覆す条件: 実アプリのクエリ分布で導出可能な割合が低すぎると実測されたら。escape hatch が多数派なら、それを既定とは呼べない。
D-4. 主体を ambient にしない
境界を越えるすべての効果は、principal を明示で — capability として — 受け取る。「認可検査に到達せずに実行できる効果」はコンパイルエラーであるべきで、最低でも後述の G-3 が実行時に数える。CVE-2025-29927 はこの失敗の教科書的な形だ: 認可を経路の途中の層に置くと、層を迂回する入力は認可ごと迂回する。露出は効果の属性であって、経路の属性ではない。
覆す条件: この儀式のせいで開発者が全能の principal を 1 個作って全部に渡すようになったら — ambient authority の再発明である。(これ自体もゲートが見張れる: 全効果の principal が同一なら警告すればよい。)
D-5. HTML が土台、JavaScript は強化
これはイデオロギーではなく部分故障工学 — P-5 への答えである。コードは毎回ネットワーク越しに届き、そして届かないことがある。劣悪な回線、企業のプロキシ、拡張機能、あるいは単なる遅さ。form が HTML として仕事をするアプリは配布の故障に対して縮退し、縮退先の無いアプリは全損する。初回表示と主要な書き込み経路は HTML + HTTP で完結し、JavaScript はそれを速くする。
覆す条件: そのアプリ級に意味のある縮退形が存在しないなら(エディタ・ゲーム・地図)。これは D-1 が吸収する — 該当機能だけが「強化必須」を宣言すればいい。
D-6. 同じ計算を二度しない
hydration — サーバがすでに実行したものを、送られてきた結果を再構築するためにクライアントがもう一度実行する — と、VDOM の再レンダ — 値が 1 つ変わったから部分木を再実行して差分を取る — は、同じ病気の 2 つの顔である。依存を追跡していないから、再計算で穴を埋めている。起動はサーバの仕事の続きから再開し(resume)、更新は依存グラフ(シグナル)を流れる。再計算はモデルではなく例外である。
正直な注記をひとつ。resumability の最先端である Qwik は、公開 Web で 672 サイトである。普及の不振は設計の反証ではないが、「直列化可能な境界を人間に規約で維持させる」ことが成立しない証拠ではある。だから D-9 抜きの D-6 は Qwik の再演になる。
覆す条件: 直列化 + 復元のコストが再実行を上回る形が支配的だと実測されたら。そのときは再実行が正しい。
D-7. 進化は第一級の主題である
アプリケーションとは「今のコード」ではない。今生きているすべての版のコードと、すべての既存データである。したがって: (1) スキーマ移行は既存データ全件に対して型検査される — 先例は Lamdera — (2) 境界はスキュー窓(どの版のクライアントまで生かすか)を宣言し、ゲートが窓内の全組み合わせを検査する、(3) expand/contract の二段デプロイは部族の知識ではなくフレームワークが知っている手順になる。
覆す条件: 無い。これはトレードオフではなく現実の記述である。高すぎる実装はあり得るが、問題そのものは消えない。
D-8. ゲートはフレームワークに同梱される
上のすべての主張は、それを検査するゲート(次節の 7 つ)と対で出荷される。ゲートは、pin された既知の乖離が直ったことも検出する。さもなければ、もはや真ではない宣言が溜まっていく。
D-9. これはライブラリでは作れない
ここまでの判断は、言語そのものに 4 つの性質を要求する: (1) 直列化可能性が型で判定できる — どの値が境界を越えてよいか、(2) 配置が効果として型に見える — server-only の効果は client 配置のコードから呼べない、(3) 描画が決定的である — G-1 の前提、(4) スキーマの版が言語の値である — D-7 の前提。現行世代は (1) と (2) を文字列 "use server" と lint で近似している。文字列の存在は性質の成立ではない — CVSS 10.0 の境界 CVE はその近似の請求書である。ゆえに最高のフレームワークはコンパイラ協調である: 言語そのものか、型付きサブセット + コード生成か。
覆す条件: TypeScript の型システム(あるいは将来の効果に相当する言語機能)が (1) と (2) を健全に表現できるようになったら。
落ちてくる形
9 つの判断を 1 つの物体として書き直すと、7 層になる:
- コンパイラ層 — 1 つのプログラムを複数ターゲット(server / client / edge / worker)に分割して配る。配置は機能ごとの宣言。境界を越える値は型で検査
- プロトコル層 — 境界のワイヤ形式: 名前・版・スキーマ・型に出る故障・冪等性。敵対的入力のパーサとして設計される
- データ層 — 記録の本体 + 生きているクエリ。無効化は導出。トランザクションは楽観更新の巻き戻しまで含めて端から端
- 主体層 — capability が「誰として」をすべての効果に運ぶ
- 表示層 — HTML 土台・resume 起動・依存グラフ更新・ストリーミングが通常営業
- 進化層 — 検査された移行・宣言されたスキュー窓・インストールベースの管理
- 検査層 — ゲート群。この層が他の 6 層の主張を検査する
7 つのゲート
| # | ゲート | 何を検査するか | 今日誰が出荷しているか |
|---|---|---|---|
| G-1 | 描画一致 | 同一入力 ⇒ サーバとクライアントが同一の木を出す。診断できない差は既知の乖離として pin し、その解除条件も機械が検査する | 誰も(React は開発時警告・本番は黙って修復) |
| G-2 | 境界互換 | デプロイ候補が、スキュー窓内のすべてのクライアント版からの呼び出しを受けられる — protobuf の互換ゲートを UI 境界に適用した形 | 誰も(Vercel Skew Protection は問題を迂回するルーティングであって、検査ではない) |
| G-3 | 認可被覆 | 境界を越えるすべての効果が principal 検査に到達している — grep ではなく実行時に数える | 誰も |
| G-4 | 無効化健全性 | 書き込みの後に古い読みが返る経路が存在しない。導出クエリは構成的に成立し、「腐ってよい」宣言つきの読みには毒入りの入力でテストを当てる | 誰も |
| G-5 | 移行 | 移行関数が既存データ全件を食える。そして戻せる | Lamdera が型レベルのみ |
| G-6 | 予算 | bundle サイズ・操作レイテンシ・アクセシビリティが数字として CI を落とす | 外付けのみ(Lighthouse CI など) |
| G-7 | JS 無し通過 | form と主要動線が JavaScript を切った状態で仕事をする — D-5 の検査 | 誰も |
7 つ全部に共通する設計規則が 2 つ。どのゲートも何件の検査が走ったかを assert する — 「全部緑」が空のテスト集合から生成されてはならない。そしてどのゲートも「オラクルが間違っている」欄を持つ — それが無いと、最初の正当な乖離がテストの削除で解決される。
分かっていないこと
- Q-1. 配置宣言は安定するか。同じ機能がオフラインでは local-first、オンラインではサーバ後ろ盾を要求するなら、静的な宣言は道具として間違っているかもしれない
- Q-2. 実世界のクエリのうち、無効化を導出できる割合はどれだけか。誰も測っていない。escape hatch が多数派なら D-3 は既定にできない
- Q-3. local-first × 認可。クライアントが楽観適用済みの書き込みを、サーバが拒否することがある。「サーバは拒否できる」と「クライアントは待たない」の合成が生む UI の意味論を、一般的に定義した者はまだいない
- Q-4. スキュー窓の権威は誰にあるか。開きっぱなしのタブは無限に生きる。窓を宣言しても(D-7)、窓の外での振る舞い — 強制リロードか、縮退か — は未定義のまま
- Q-5. 言語は非決定性(時刻・乱数・ロケール・浮動小数)をどう封じるか。G-1 のオラクルはそれが前提で、1 箇所漏れれば乖離リストが埋まる
- Q-6. 最大の未検査の主張は、この文章自身の背骨である。「ゲートを同梱すると、型と普通のテストでは出ない率で欠陥が出る」— これは主張だ。誰かがこれを作り、毒入りの入力を食わせるまで、主張のままである
この文書はどう間違いうるか
各判断には反証条件を付けた。全体に対しても 2 つ:
- ゲートを実装し、実在するアプリを 1 本載せ替えて、毒入りの入力を流す — G-1〜G-7 が型と既存テストを超えるものを何も捕まえなければ、この文章の背骨は web には間違いだった
- ブラウザが状態同期と版管理をプラットフォーム機能として持つようになったら、あるいは共有メモリ付き WASM が境界の形そのものを変えたら、P-1〜P-8 の列挙は偽の前提から出発している。やり直しである
私は趣味でプログラミング言語を作っていて、この定義がそれに合うのかというのが自明な次の問いだ。その問いはここでは意図的に立てなかった。答えに都合よく書かれた定義は、定義ではないからだ。
Sources
- React: Critical Security Vulnerability in React Server Components (CVE-2025-66478) · Next.js advisory · OX Security analysis
- Vercel security bulletin: CVE-2025-55184 / CVE-2025-55183
- Next.js Version Skew Protection — the complete guide
- PkgPulse: The State of Server Components in 2026
- Kanopy: TanStack DB vs ElectricSQL vs Zero (2026) · PkgPulse: Reactive Sync Engines 2026
- Alternatives to Qwik 2026 · Qwik docs: Resumable
- Lamdera · Lamdera v1.1.0: auto-generated migrations
- Convex: The “full-stack framework” fallacy
- HTMX in Production 2026 · How does Phoenix LiveView work