← ラボノート

スマートフォンから訪れる人のために、読み込みを軽くする

私たちAI Maker Labは、AIコーディングエージェントと一緒にブラウザー運転シム「Seat Time」を開発しています。2026年10月19日の週、ガレージを開くにはgzip圧縮済みのJavaScriptを2.10 MBダウンロードする必要がありました。その後、ブラウザーが解析するJavaScriptは7.9 MBありました。エンジン音も雨もカートコースもあり、ステアリングもブラウザーにつながっていました。ただ、スマートフォンでは、まずその入口を通る必要がありました。

私たちが望んだのは、もっと早くガレージを開くことでした。エージェントにソースマップを調べてもらうと、ゲームの塊で目立ったのは翻訳文でした。メッセージの元ソースは7.4 MB。ゲーム画面全体の元ソースは1.4 MBでした。これはソースマップ上の量で、圧縮後の転送量ではありません。車両より先に調べるべき、意外な場所が見つかりました。

まず読み込みの流れを見えるようにしました

ページ表示から最初の操作まで、Google Analytics 4に段階別のイベントを追加しました。これで読み込みの順序を観測できます。ただし、変更前後で訪問者の滞在が延びたかを示す比較ではありません。その比較は、保存された記録には今もありません。

ビルドでは、ゲーム本体と静的importが連れてくる依存先をまとめて測りました。ここでは、この集合を「静的依存一式」と呼びます。入口のファイルだけでは、依存先が見えません。圧縮サイズだけでは、取得後にコードを解析・実行する負荷も見えません。web.devのコード分割の解説も、この違いを説明しています。

約7,700件のメッセージが付いてきました

3か所の参照が、約7,700件のメッセージを残していました。2か所ではメッセージ集全体を渡していました。もう1か所では、計算したキーでメッセージを選んでいました。これが未使用メッセージの除去を妨げた、というのがソースマップからの私たちの診断です。コード上の小さな便利さが、大きなダウンロードになっていました。

参照を明示すると、残るメッセージは2,508件になりました。それだけで圧縮後の静的依存一式は2,102 KBから1,565 KBに減りました。さらに、条件付きで表示する29個のパネルを、開くときに読み込む形へ変えました。両方の変更後は1,370 KBです。出発点からgzipで約3分の1の削減でした。

パネルの機能は残しています。変えたのは、コードが届くタイミングです。Viteの動的importの説明にある仕組みで、必要になるまで読み込みを遅らせます。メッセージ集全体を再び渡さないためのチェックも追加しました。次の便利な補助関数で、同じ重さが戻らないようにするためです。

記録されたJavaScriptの圧縮サイズは2,102、1,565、1,370 KBへ減少し、後のマニフェスト変更では約1,223 KBになりました。
記録された各ビルドの値をKB gzipに丸めています。後のマニフェスト比較は1,374.0 KBが基準です。各段階の差を厳密に足し合わせる図ではありません。

車の位置を変えず、データをほぼ半分にしました

車両ファイルの削減では、最初の方法を取りやめました。量子化は、値を表す精度を下げる処理です。試したところ、車輪半径、ミラーの平面、メーターの帯域など、15件の契約チェックが失敗しました。ファイルを小さくするために、守るべき形状を変えるわけにはいきません。

その位置の量子化は外しました。採用したmeshoptimizerベースの処理では、頂点位置をビット単位で維持しています。一方、陰影に使う方向を表す法線は、2⁻¹⁰の格子に丸めました。26個の車両ファイルは21,419,496バイトから10,989,652バイトになりました。ほぼ半分になったのはファイルの重さで、頂点数ではありません。

正確に保ったのは位置であり、全属性ではありません。meshoptimizerの文書でも、量子化は精度を選ぶ別の処理です。また、本番ではこのバイナリモデルをgzipやBrotliなしで配信していました。今回削ったのは、実際に転送されていたバイトです。存在を確認していない転送圧縮を当てにしたわけではありません。

音は役割に合わせて届けるようにしました

音声も、早い段階でまとめて届いていました。旧ローダーが運転開始時に取得する集合は13.96 MBでした。効果音が12.16 MB、エンジン音が1.80 MBです。私たちは役割別の読み込みに変え、一度に全部を要求しないようにしました。承認済みのWAV音源から、圧縮したOpusファイルも作りました。

変更後、運転開始時のブラウザー実測は0.97 MBでした。ただし、変更前は旧ローダーの取得集合から算出した値です。同条件のブラウザー測定を前後で並べた比較ではありません。変換した155ファイル全体では25.3 MBから3.1 MBになりました。継ぎ目なく繰り返す8個のループはWAVのままです。

運転開始時の音声は、旧ローダーの取得集合から算出した13.96 MBに対し、変更後のブラウザー実測では0.97 MBでした。
運転開始時に取得する音声を減らしました。変更前は取得集合からの算出値、変更後はブラウザー実測です。

派生ファイルと承認済み音源の対応は、明示的に残しました。対応する派生記録がないOpusファイルは再生できません。これは設計上のルールで、試聴結果ではありません。Opusの標準仕様はコーデックを定義しますが、私たちの変換音が同一に聞こえることまでは証明しません。この配信変更について、新しい試聴判定の記録はありません。

マニフェストには読まない情報がありました

環境マニフェストも、実行時に必要な情報へ絞りました。ゲームが実際に読むフィールドだけを取り出した、小さな文書に置き換えています。元サイズは3,337 KBから962 KBへ減りました。この比較での圧縮済み静的依存一式は1,374.0 KBから1,223.4 KBです。削減量は150.6 KBでした。

基準値は、先ほどの1,370 KBという記録とは少し違います。数字を無理につなげず、それぞれの比較として残しました。私たちには共通点が見えてきました。どこかでは役立つ情報でも、その瞬間には不要なものがありました。翻訳の補助処理、閉じたパネル、制作向けメタデータが、ガレージまで一緒に来ていたのです。

クリックで先に始めても、短縮は小さめでした

次は、クリックした時点でゲームの取得を始めました。サーバーのデータ要求と並行して進めます。ゲームの塊は約370 msで取得を開始し、約480 msのデータ応答より先に動き始めました。処理間の待ちを減らせますが、最終的に必要な量は変わりません。

Slow 4Gと処理速度4倍低下のエミュレーションでは、短縮は控えめでした。全体読み込みの中央値は15.85秒から15.23秒へ変化しました。クライアント側の画面遷移では13.75秒から13.47秒です。丸めると、それぞれ0.6秒、0.3秒の短縮でした。Chromeのネットワーク制限の文書が説明するのはエミュレーションです。訪問者のスマートフォンでの実測ではありません。

この変更は残しました。ただ、残りのダウンロードが消えるわけではありません。ガレージを使える状態にするまで、なお約1.9 MBが必要でした。早く始めることには効果がありました。それでも、送る量を減らす仕事は残りました。

サーバーでもimportの問題が繰り返し起きました

サーバーでも、不要な処理が早すぎる段階に入り込んでいました。10月22日から25日にかけて、同じ形の問題が3回起きました。ただし結果は別々です。デプロイの拒否、公開前の停止、そして利用者に届いた不具合でした。3回とも障害で停止したと書けば、2回は防げた事実が消えてしまいます。

10月22日は、Cloudflareが初期化エラー10021でデプロイを拒否しました。本番は前の版を維持しました。外観アイテムの検証が、描画用マニフェストを経由して車両物理へ達していました。必要だったのは車両の識別子一覧だけです。しかし、そのimportで初期化中に車両仕様を構築していました。Cloudflareの起動時間の説明は、このエラーとグローバル領域の過剰な処理を結び付けています。

10月24日は、起動チェックがコース一覧の再流入を検出しました。今度はラップ検証がコースのメタデータを必要としていました。依存先を単純なコース長の表へ置き換え、一覧へ続く経路を外しました。その版がプレイヤーへ届く前に、公開を止められました。

10月25日は、プレイヤーにも不具合が届きました。新しいサーバー実行環境では、ログイン済みの競技読み取りに4〜6秒の処理時間を使っていました。実際の経過時間は7〜17秒です。初期化済みの要求は約20 msでした。チャレンジ画面ではラウンドの利用状況を読み込めませんでした。ローカルの解析では、モジュール読み込み中に全10コースを構築していました。競技の定期処理もメモリー上限を超えました。

10月22日はデプロイ拒否、24日は公開前に停止、25日はチャレンジ画面の不具合。3件のうち利用者に届いた失敗は1件です。
同じ依存関係の誤りが、異なる段階まで達しました。利用者に届いたと記録されているのは最後の1件です。

タイムアウトでは処理を止められませんでした

設定した期限では、この失敗を救えませんでした。モジュールの処理は同期的に実行されます。タイマーの処理も動かすイベントループを、その作業が占有していました。750 msの開始予算も、8秒の読み取り期限も割り込めません。タイムアウトを待つ間も、コースの構築完了を待っていたのです。

軽量な識別子を形状から分離し、必要なときだけコースを構築する形にしました。比較用のフィンガープリントも事前計算した表にしました。それを得るためだけに、コースを再構築する必要はありません。importの境界とフィンガープリントのずれをチェックで守ります。競技用処理のコールドimportは、ローカルで16.4秒から0.5秒になりました。形状と24個のランキング用フィンガープリントは、バイト単位で同一でした。ただし、修正後の本番処理時間は保存された記録にありません。ローカルの結果を、そのまま本番性能とは呼べません。

小さな入口の詰まりも直しました

「ストレージを利用できない」というエラーは、localStorageの容量不足でした。保存ラップをIndexedDBへ移し、バイナリ表現にしました。1周あたり約1.3 MBのテキストが、約180 kBになりました。これは自分と競走できるゴーストの記録です。MDNのストレージ解説は、文字列専用のWeb Storageと、より大きなブラウザー管理ストレージを区別しています。IndexedDBにも容量制限があるため、保存が無制限になったわけではありません。

サーバーでは、アクセス規則の読み取りをまとめ、2回の対話的トランザクションを置き換えました。リアルタイムの監視で拾えなかったエラーを受け、Workers Logsも有効にしました。それぞれ別の待ち時間や観測の問題を扱っています。全部を一つの高速化率にまとめたり、この週に継続利用が改善したと主張したりはしません。

ガレージには、まだ削れる重さがあります

この作業で、ダウンロードは小さくなり、サーバー不具合の理由も明確になりました。一方、使えるガレージまで約1.9 MBは残りました。訪問者の継続利用の前後比較も、最後のimport修正後の本番処理時間もありません。小さくなった数字は、何を変えたかを示します。誰がどれだけ長く遊んだかまでは示しません。

スマートフォンでガレージを開く。次は、グリップの変化をステアリングが正しく伝えるようにしたいと考えました。

前回:ブラウザーのタブにつながるステアリング · 次回:グリップはステアリングが軽くなる感触

注記と出典

測定値 — 2026年10月19〜25日のブラウザービルド記録:gzipで2.10 MB、解析対象7.9 MB。ソースマップではメッセージ7.4 MB、画面部分1.4 MB。静的依存一式の圧縮サイズとメッセージ数も同時期の記録です。元ソースと出力コードは異なる表現を測っています。後のマニフェスト比較は独自の基準値です。今回は再測定せず、私たちの保存記録を使いました。

測定値 — 素材の記録:26モデルと本文のバイト数、失敗した15件の形状チェック、正確な頂点位置と丸めた法線。音声の変更前は旧取得集合からの計算、変更後はブラウザー測定です。設計上の選択 — 派生音声は承認済み音源の記録と対応させ、8ループはWAVを維持します。試聴判定の記録なし — 配信変更の数字は音質比較ではありません。

測定値・エミュレーション — クリック開始の値はSlow 4Gと処理速度4倍低下の条件です。測定値・本番 — 新規実行環境と初期化済み環境の競技要求は10月25日の観測です。ローカル解析のみ — 16.4秒から0.5秒のimportと、形状・フィンガープリント比較は本番の受け入れ結果ではありません。私たちの解釈 — メッセージの保持と同期処理が、それぞれの依存問題を説明します。自社リリース記録に基づく事実 — 先の2件は、デプロイと公開をそれぞれ止めました。修正後の本番処理時間は保存記録にありません。

測定値 — ゴーストの保存サイズは概数です。自社実装記録に基づく事実 — 読み込み段階の計測、アクセス規則の一括読み取り、ログ変更。公開資料は仕組みの説明であり、私たちの結果の証明ではありません:JavaScriptの分割、Viteのimport、meshoptimizer、Opus、ネットワークのエミュレーション、Workerの起動、ブラウザーストレージ。2026年11月6日に閲覧しました。製品名は使用ツールの識別であり、推奨を意味しません。調査内容の整理、執筆、各言語への調整にはAIを使用しました。

あわせて読む