公開
Lighthouse 100×4 と Observatory A+ を複数アプリで取るための定石
複数のアプリで繰り返し効いた手順。Three.js の動的 import、インライン polyfill の無効化、Next のフォント方針、CSP から unsafe-inline を外すこと、ビーコンに SRI を付けない理由、中央値判定の CI ガード。
ポートフォリオに載せているアプリの Lighthouse と Mozilla Observatory は、計測日つきで data.ts に記録しています。2026-09-30 の計測では公開 19 本のうち hyper-tetris・tricking-3d・snippet-sprint がモバイルで 100/100/100/100、Observatory A+ は lumen-bloom・skydial・snippet-sprint・css-atelier・glsl-atelier・hyper-tetris・tricking-3d・chronoscroll・somewhere-now です。繰り返し効いた手順を並べます。
Performance: 最初の JS を小さくする
バニラ Vite + Three.js のアプリは、Three.js を初期バンドルから外すだけでモバイルの Performance が大きく動きます。
- hyper-tetris:
src/boot.tsがawait import("./main.js")するだけの構成にして、エントリ 600KB → 2.2KB、TBT 410ms → 0ms、Performance 82 → 100 - tricking-3d: 683KB の解析本体を
src/app-main.tsに分離し、src/main.tsは 2.5KB のタイトル画面。スタート画面の HTML/CSS はindex.htmlにインライン化 - snippet-sprint: ゲーム本体を初回のポインタ/キー入力で読み込む。アイドル時に先読みしたところ Three.js がコールドロードの計測に入ってしまい、逆に下がったのでやめた。スタート画面のピルを HTML に事前描画して CLS 0.125 → 0
Next.js アプリは 98〜99 が天井で、100 は構造的に難しいと感じています。service-anatomy では日本語 Web フォントの @font-face 群だけで約 190KB の CSS がレンダーブロッキングになり Performance が 72 まで落ちたので、日本語はシステムフォントに戻して 95 まで回復しました。欧文だけ next/font で display: "optional" にしています。swap だと h1 の LCP がフォント到着の再描画に引きずられ、CI の判定が不安定になりました。
Accessibility / Best Practices / SEO の小さな項目
100 に届かない残りは、たいてい次のどれかでした。
- viewport の
user-scalable=noやmaximum-scale(Accessibility) - favicon の 404(Best Practices)。
data:の SVG で解決 robots.txtがない(SEO)- 補足文のコントラスト不足。このポートフォリオでは slate-500 がダーク背景で 3.3〜4.1:1 しかなく、
--color-muted: #8091a9を定義して 4.8:1 以上にした
2026-09-29〜10-01 には全リポジトリで axe の違反をゼロにする一括点検もしました(css-atelier 819 → 0、lifeplan-simulator 494 → 0、hyper-tetris 85 → 0 など)。
Observatory: unsafe-inline を外せるかどうか
Observatory v2 の 12 項目で差がつくのは CSP です。script-src から 'unsafe-inline' を外せると A+(115〜120)、外せないと -20 で B 前後になります。
Vite アプリでは build.modulePreload.polyfill: false が鍵でした。これがないとインラインの polyfill スクリプトが入り、'unsafe-inline' が要ります。インラインの <style> も外部 CSS に移します。hyper-tetris と tricking-3d はこの手順で B+ から A+ になりました。
Next.js アプリはブートストラップ(self.__next_f.push)がインラインなので 'unsafe-inline' が外せず、nonce 方式も OpenNext では使えません。service-anatomy・ai-primer・acro-finder・このポートフォリオが B(75)なのはそのためで、失点は CSP -20 と SRI -5 の 2 項目だけです。
SRI の -5 は意図的に受け入れています。Cloudflare Web Analytics の beacon.min.js はバージョンのない URL を Cloudflare 側が差し替える運用なので、integrity を固定するとビーコンだけ黙って止まります。
CI で守る
service-anatomy と ai-primer の CI には Lighthouse の回帰ガードがあります。npm start で本番ビルドを起動し、3 回計測した中央値で判定します(閾値 Performance 80 / Accessibility 95 / Best Practices 90 / SEO 95)。共有ランナーでは同一コードで 94 → 72 まで振れたことがあり、単発の計測では判定になりませんでした。
一覧の数値を更新するときは median-of-3 で測り、measuredAt を必ず添えています。古い数値が今の数値のように見えるのが、この種のページでいちばん避けたいことです。
参考
- hyper-tetris commit 8c961b5、tricking-3d commit 3cb6af8、snippet-sprint commit c6ec427・b8143c9(動的 import と CLS の記録)
- service-anatomy
scripts/lighthouse-ci.mjs、src/app/layout.tsxのフォントコメント - my-apps-portal
data.ts— 各アプリのlighthouseScores/securityHeaders(計測日つき)