# apps/web: エラーフォールバック起因の First Load JS 削減(HashiBow 静的 SVG 化 + 記事ルート error.tsx 削除)
- 起票日: 2026-07-12
- 関連: `feat/error-fallback-bundle-size` ブランチ(bundle-size CI レポート整備)
- ステータス: 完了
## 起票理由
`TURBOPACK_STATS=1 bun run build` の計測(`.next/diagnostics/route-bundle-stats.json`、gzip 前)で全ルートの First Load JS が 560 KiB 超あり、ルート間の差が小さい(= 共有チャンクが支配的)。調査の結果、共有チャンク 576.7 KiB のうち約 89%(約 512 KiB)は react-dom + Next.js App Router ランタイムの固定費で userland からは削減できないが、アプリ側起因として次の 2 点が見つかった。
1. **HashiBow(約 23 KB のインライン SVG マスコット)が全ルートのクライアント JS に同梱されている**。`app/error.tsx`(`'use client'` エントリ)が `BaseLayout` + `ErrorFallback` → `HashiBow` を静的 import しているため、滅多に表示されないエラー UI が全ルートの First Load に 34.4 KiB 入っている(チャンクの 65% = 22.3 KiB が SVG パスデータ)
2. **記事ルートは同じ 34.4 KiB をほぼ丸ごと二重ダウンロードしている**。`[userName]/articles/[slug]/error.tsx` が root と同じ import を持ち、Turbopack がクライアントエントリごとにチャンクグループを作って共有モジュールを複製するため、内容がほぼ同一のチャンク(長い文字列リテラルの 102/103 が一致)が route 固有分として追加される
## 調査結果
### ルート別 First Load JS(2026-07-12 時点、gzip 前)
| ルート | First Load JS |
| ----------------------------- | ------------- |
| `/[userName]/articles/[slug]` | 626.5 KiB |
| `/` , `/page/[page]` | 603.5 KiB |
| `/about` | 590.9 KiB |
| `/_not-found` | 563.2 KiB |
| ユニークチャンク合計 | 694.5 KiB |
### 共有チャンクの内訳(全ルート共通 9 チャンク = 576.7 KiB)
| チャンク | サイズ | 中身(文字列シグネチャから特定) |
| ---------------------- | ------------ | ------------------------------------------------------------------------------------------------ |
| `39tmvtji6sug0.js` | 226.3 KiB | react-dom 本体 + Flight client + Next ルーター断片 |
| `3lo6xk-xle5hd.js` | 138.0 KiB | Next App Router クライアントランタイム(prefetch / scroll restore / server-action) |
| `27gywtrp1z5ef.js` | 56.7 KiB | react-dom 残部 + next/image クライアント側 |
| `39xvxhw2cucl3.js` | 54.6 KiB | Next 共有ユーティリティ |
| **`09phro5ju3clc.js`** | **34.4 KiB** | **アプリコード: ErrorFallback + BaseLayout 一式(うち 22.3 KiB が HashiBow の SVG パスデータ)** |
| `08737ep5_8k6w.js` | 25.0 KiB | react パッケージ + jsx-runtime |
| `41mki-bgq2t7t.js` | 21.5 KiB | nprogress + Clarity + @vercel/analytics + speed-insights |
| turbopack ランタイム | 10.6 KiB | 固定費 |
| `15cn46ua6_2c-.js` | 9.6 KiB | アプリコード(layout 配下の Provider 類) |
記事ルートはこれに加えて `2b7b8i7kh_0_r.js`(34.4 KiB、`09phro5ju3clc.js` とほぼ同一内容)と route 固有チャンク(30.4 KiB)を読む。
### 原因のメカニズム
- `HashiBow` / `ErrorFallback` / `Button` 自体は `'use client'` なしのサーバー安全なコンポーネント。`error.tsx`(クライアントエントリ)から import されることでクライアントバンドル入りしている
- Turbopack は `'use client'` エントリごとにチャンクグループを作り、グループ間で共有モジュールを複製する。root の `app/error.tsx` と記事ルートの `error.tsx` が同じコンポーネント群を import しているため、同内容チャンクが 2 つできる
- `not-found.tsx` は RSC なので `ErrorFallback` を使っても JS には入らない(HTML に SVG がインライン展開されるだけ)。対応不要
- react-tweet(+swr)と tocbot はすでに dynamic import 済みで First Load 外。対応不要
## 設計方針
| 論点 | 決定 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HashiBow の追い出し方 | インライン JSX SVG をやめ、`public/hashi-bow.svg` の静的ファイル + `
` 参照にする。`HashiBow.tsx` は `
` を返す薄いラッパーとして残し、呼び出し側(ErrorFallback / Storybook)は無変更 |
| `
` 化が成立する根拠 | HashiBow は hex 固定 6 色のみのフルカラー SVG。CSS 変数 / currentColor / テーマ依存 / インタラクションなし(DESIGN.md の「マスコットはフルカラー SVG」ルールとも整合) |
| dynamic import 案の不採用 | `error.tsx` 側を `next/dynamic` にする案は、エラー発生時(= ネットワーク障害の可能性が高い場面)にチャンク fetch が必要になり本末転倒。静的 SVG なら画像が落ちてもテキストと再試行ボタンは即描画される |
| オフライン時のデグレ | 許容する。ユーザー回線断でエラー UI が出た場合のみ画像 fetch も失敗して alt 表示になるが、テキストと再試行ボタンは JS 同梱のため必ず描画される。blog-api 障害や JS ランタイムエラー(大半のケース)では回線は生きており画像は表示される。`` での先読みは転送量が復活するため採用しない |
| 記事ルート `error.tsx` | 削除。root の `app/error.tsx` が同一 UI(BaseLayout + ErrorFallback)で受けるため、ユーザーから見た表示は変わらない |
| 記事ルート `loading.tsx` | 残す。BaseLayout import による残差重複(約 11 KiB)が残るかはビルドで実測し、残る場合のみ対処を検討 |
| about ページの HashiMascot | スコープ外。マウス追従のインタラクティブ SVG(ソース 5.9 KB)で route 固有チャンク側にあり、共有チャンクを太らせていない |
## 実装フェーズ
- [x] Phase A: HashiBow の静的 SVG 化
- [x] Phase B: 記事ルート error.tsx の削除
- [x] Phase C: 計測・検証
### Phase A: HashiBow の静的 SVG 化
1. `src/components/HashiBow.tsx` の `