モデル一覧を開いただけなのに、仕事が増える

モデルを選ぶ画面には、モデルの一覧が必要です。Kumyu Model Councilでは、その一覧を更新するために、画面全体の状態を集めるスナップショット処理を呼んでいました。既存の便利な応答を受け取り、その一部だけを使う実装です。

画面は普通に動きます。だから見つけにくい。余計な処理にも実装があり、正常な応答があり、その結果を表示できる呼び出し元があります。

今回、Kumyuのモノレポにある全8アプリを調べ、4つの具体的な仕組みを変更しました。共通していたのは、モデルを選ぶ、記事を開く、一覧を絞る、ページを再訪するといった小さな操作に、必要以上の仕事が付いていたことです。

以下の数値は、ローカルビルドと固定入力のテストによる結果です。配信中のソースとの照合と本番ブラウザーの計測は残っており、本番のLCPやINPの改善は未確認です。

以下のサイズ・操作回数・ビルド・テスト結果の条件は、ソースの基点 641e4afb と同じ入力での変更前後、Node 26.7.0、zlib 1.2.12、esbuild 0.28.2、TanStack React Router 1.170.41です。その後のmainへの依存更新の統合にはRouter 1.170.44も含まれ、この比較の対象外です。統合後の最終依存で回帰がないことは、まだ確かめられていません。

応答を速く作る前に、質問を小さくする

モデル一覧の更新先を、既存の list_models 処理へ変更しました。必要な問いに答える処理は、すでに存在していました。新しいAPIやキャッシュ層を足す必要はありません。

前後比較には、同じモデルカタログと既定の選択モデルを使い、議論データと設定済みのプロバイダーキーがない固定入力のテスト(fixture)を用いました。数えたのはJSONを直列化したときのUTF-8バイト数です。圧縮前の応答本文であり、ネットワーク上の転送量ではありません。

モデル一覧のfixture 変更前:スナップショットJSON 変更後:モデル一覧JSON 削減率
英語 4,747 B 591 B 87.6%
日本語 5,312 B 591 B 88.9%

この読み取りで実行するSQLも、fixtureでは2回から1回へ減りました。モデル一覧と既定の選択は前後で一致しています。返す情報を減らすときは、この確認が欠かせません。必要な選択肢まで消せば、小さな応答は簡単に作れてしまいます。

ただし、Model Councilが88.9%速くなったわけではありません。この表に、ネットワーク待ち、認証、描画、モデル提供元の応答時間、本番のデータ量は含まれません。確認できたのは、モデル一覧を更新する際に、要求する情報とDBの仕事を減らせたことです。

ほかの画面でも使える問いがあります。この画面が知りたいことに答える、一番小さな既存処理はどれか。 画面全体を返す便利な処理は、小さな部品すべてから呼ばれるようになると、不要な依存にもなります。

静的HTMLなのに、データはもう一度読めてしまう

Kumyu Learnの記事本文は、すでに静的HTMLへ生成しています。サーバーのLLM、外部フォント、JavaScriptによる本文の組み立てを待たずに読めます。各ルートのデータも、記事の全コーパスから分離しています。

これは残すべき良い設計です。ただし、その後にReactがhydrateし、ルーターのloaderが動くときの振る舞いまでは、自動で決まりません。

現行記事と保存版の記事ルートは、データの鮮度を個別に指定していませんでした。TanStack Routerの文書上、staleTime の既定値は0です。再利用できるloaderの結果もすぐに古い扱いになり、明示的な router.load() で再検証が起き得ます。

Learnで実際に使う makeRouter をテストし、プリレンダー時の固定時計から、hydrate後と同じクライアントモードへ切り替えて再現しました。その後に履歴を読み込むと、変更前は記事のloaderがもう一度動きます。変更後は受け取った結果を再利用し、2回から1回になりました。記事のslug、版番号、言語が変われば、それぞれのデータを取得することも回帰テストで確認しています。

静的な現行記事と保存版の記事ルートへ、次の指定を加えました。

staleTime: Infinity

この Infinity が決めるのは、再利用可能なloaderの結果をルーターが古いと判断するかどうかです。HTTPキャッシュの保持やガベージコレクションは別の仕組みです。開いたままのページに将来の改訂が自動で届くわけではなく、ドキュメントの再読み込みや明示的な無効化には、それぞれの振る舞いがあります。

ビルドで作る記事データだから、この鮮度方針を選びました。残高、編集中の設定、進行中の会話へ、そのまま当てはめる設定ではありません。

根拠:TanStack RouterのData Loadingの既定値と自動再取得を無効にする節。上記の回数はローカルの回帰テストによる結果です。

検索のたびに、同じDOMを探していた

Kumyu Catalogは、一覧そのものを初期HTMLに含めています。JavaScriptが加えるのは、絞り込み、保存、比較対象の選択、並べ替えです。この構成は維持しました。

見直したのは、絞り込み処理の中身です。検索用の文字列を繰り返し正規化し、対応するカード要素をその都度探していました。さらに、並べ替えの条件が変わっていなくても、カードをappendしていました。検索語だけが変わった場面でも、順番に関するDOM操作が発生していたわけです。

変更後は、正規化した検索メタデータとカード要素の参照を一度準備して再利用します。ソート条件が変わったときにDOMの順序を変え、表示・非表示だけが変わったときは、その順序を触りません。検索対象のメタデータや作品を減らす変更ではありません。

Catalogの実スクリプトをローカルのVMで実行し、各言語の実データ64件と検索語20件で操作を数えました。レイアウトや描画、入力遅延を再現するブラウザーの測定ではありません。

数えた操作 変更前 変更後
検索文字列の正規化:各言語 1,300回 84回
初期化時のカード移動:各言語 64回 0回
英語20検索でのカード移動 270回 0回
日本語20検索でのカード移動 251回 0回

検索時の移動が0回でも、ソートをやめたわけではありません。ソート条件が変われば並べ替え、検索語だけが変わったときの不要な移動をなくしています。

もう一つ、モーションのデモに使うCSSを、そのデモがあるページに限定しました。デモのない生成ルート171件から参照を外したファイルは、圧縮前11,682 B、gzipで3,031 Bです。一方、検索の実装はgzipで246 B増えました。再利用する準備コードが少し増える代わりに、繰り返す操作と不要なCSSへの参照を減らしています。実際の転送削減量は、ブラウザーのキャッシュや圧縮にも左右されます。

Catalogの既存検査では、204個のHTMLと6,301個のリンク、および既存テスト16件が合格しました。出力と契約の整合性を支える結果です。入力遅延を測った結果でも、INP改善を証明する結果でもありません。

次に必要なのは、ブラウザーと一覧データを固定し、実際に文字を入力して並べ替えるときのtraceです。計算やDOM操作を減らした数は、改善を期待する理由にはなります。読者が何ミリ秒の差を感じるかは、操作を測って確かめます。

キャッシュの仕組みはあった。スクリプト2本が外にいた

Homeには、すでに内容のハッシュをファイル名へ付けるアセット処理と、その対象を長く保持するimmutableキャッシュの仕組みがありました。テーマ用とキャラクターの動き用のスクリプトは、固定名のまま、その処理の外にありました。

この2本を、既存のminifyとハッシュ付与の処理へ加えました。中身が変わると生成URLも変わり、HTMLは生成後の名前を参照します。同じimmutableアセットの配信方針へ載せられます。

同じ入力から再現できるビルドで、出力サイズも比較しました。

共通スクリプト 圧縮前:変更前 圧縮前:変更後 gzip:変更前 gzip:変更後
テーマ 1,898 B 1,049 B 752 B 496 B
キャラクターの動き 10,506 B 7,699 B 3,781 B 3,110 B
2ファイルの合計 12,404 B 8,748 B 4,533 B 3,606 B

gzipサイズの合計は20.4%減りました。ローカルで圧縮したビルド成果物の値であり、本番の転送量を測った値ではありません。Homeのビルドと191件のテストは合格しています。minifyとハッシュ付与には既存の配信経路を使い、別のキャッシュ版番号は増やしていません。

分けるべきなのは、アセットのURLと、それを指すHTMLの役目です。内容が固定されたアセットは、鮮度の有効期間中に再利用できます。HTMLには、新しいアセットURLを参照できる公開・再検証の方針が必要です。個人向けの応答を共有キャッシュへ置いてよいかどうかは、さらに別の判断になります。

根拠:MDNのCache-Controlの immutable とハッシュ付き静的資源の節。

8アプリを調べても、直す場所は同じにならない

モノレポ全体を調べることと、すべてのアプリを書き換えることは別です。具体的な根拠から変更した箇所と、別の条件で測る必要がある箇所を分けました。

アプリ 今回調べた範囲 変更または残った確認
Home 静的ページの配信と共通スクリプト 2本を既存のハッシュ付きアセット処理へ追加
Learn 静的記事ルートとルーターの鮮度 現行記事・保存版のloaderへ鮮度を指定
Catalog 検索メタデータ、カード操作、CSS 準備結果と要素参照を再利用し、モーションCSSを限定
Model Council モデル一覧の取得と応答内容 画面全体の状態取得から既存のモデル一覧処理へ変更
Channel ソース上の調査 今回の調査から本番性能の結論は出していません
Matching ソース上の調査 認証後の操作は未計測です
Computer Use スナップショットの保存先と一覧の境界 本体はすでにR2、要約はD1。一覧が増えた場合の負荷は未計測です
Agent Lab ソース上の調査 今回の調査から本番性能の結論は出していません

Computer Useの大きなスナップショット本体とDBの要約は、すでに分離していました。この境界は維持しました。カーソルで取得する一覧が増えたときに重くなるかは、別の負荷条件で測る課題です。

認証、所有権の確認、永続化も維持します。権限を確かめる問い合わせは、時間がかかるという理由だけで余計な仕事にはなりません。

応答が小さくなった。その先は、もう一度測る

性能改善の記事で書きたくなるのは、削減率の後ろに「高速化」と付けることです。今回の結果から、その言い方はできません。

JSONのバイト数、loaderの実行回数、外したCSS、入力中のtraceは、別の問いに答えます。web.devのLCP最適化ガイドも、応答、資源の発見、転送、描画を分けて調べます。ラボの結果と実ユーザーの値が異なるため、ここに実ユーザーのパーセンタイルを作ることはしません。

実装側では、Learnのフロントエンドビルド、プリレンダー、対象テスト、生成出力の検査も通しています。一方、依存auditはレジストリへ接続できず、CIはアプリのステップより前に実行基盤で止まり、このセッションではCloudflareのactive sourceを読めませんでした。これらは未確認のまま残しています。

確認できる環境がそろったら、変更前後のビルドを固定し、実行順を交互にして、画面幅・回線条件をそろえて比較します。計測中に配信中のソースが切り替わっていないことも確かめます。モデル一覧の更新、記事の再読み込みと遷移、検索、ソート、再訪という、変更に関係する操作が対象です。

普通に動く操作を、一つだけ疑ってみる

自分のアプリでも、普通に動いている小さな操作を一つ選びます。必要な情報を書き出し、実際の応答フィールド、DBの読み取り、読み込むファイル、DOM操作と比べてみてください。

減らす候補の隣には、変えてはいけない結果を置きます。モデルの選択肢が一致すること。JavaScriptなしでも記事を読めること。同じ作品が検索に出ること。指定した並び順を保つこと。アセットが変わればURLも変わること。一つの余計な処理を除いて、その条件と指標を両方確かめます。

AIエージェントは、動く画面を素早く組み立てられます。画面全体を返す応答、明示しなかったキャッシュの既定値、繰り返すDOM操作は、正常系が動くために残ることがあります。レビューで役立つのは、具体的な問いです。どの要求、SQL、バイト、操作なら、読者が得る結果を保ったまま消せるでしょうか。

今回の変更は、その問いから選びました。配信後に読者の体験を測ることが、次の証拠になります。

MENTAL MODEL / 考える順序

発表から、自分の判断へ。

一次資料

発表の主張と、論文・公式ドキュメントの条件を並べて読む。

出典

公開日は資料の日付、確認日は内容を参照した日です。コミュニティの観測は公式の確定事項と区別します。

01
公式ドキュメントweb.dev: Optimize Largest Contentful Paint ↗web.dev公開: 2020-04-30 · 確認: 2026-10-23
02
公式ドキュメントMDN: Cache-Control header ↗developer.mozilla.org公開: 不明 · 確認: 2026-10-23
03
公式ドキュメントTanStack Router: Data Loading ↗tanstack.com公開: 不明 · 確認: 2026-10-23
このブラウザ内に保存します。