モデル一覧を開いただけなのに、仕事が増える
モデルを選ぶ画面には、モデルの一覧が必要です。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