Skilore

ネットワーク最適化

Webアプリケーションのネットワークパフォーマンスを最適化する。レイテンシ削減、帯域最適化、接続管理、圧縮、プリロード等の手法を体系的に学び、高速なユーザー体験を実現する。

130 分で読めます64,869 文字

ネットワーク最適化

Webアプリケーションのネットワークパフォーマンスを最適化する。レイテンシ削減、帯域最適化、接続管理、圧縮、プリロード等の手法を体系的に学び、高速なユーザー体験を実現する。


前提知識

本ガイドを読む前に、以下の知識があると理解がスムーズになる。

  • ネットワークデバッグの基礎: curl、Chrome DevTools、tcpdump などのツールを使ってネットワーク通信を観察・計測できること。詳細は ネットワークデバッグ を参照
  • TCP/IP の基本: TCP の 3 ウェイハンドシェイク、ウィンドウサイズ、再送制御の仕組みを理解していること。詳細は TCP プロトコル を参照
  • CDN の仕組み: CDN のキャッシュ戦略、エッジサーバー、Origin Shield の概念を把握していること。詳細は CDN を参照
  • HTTP の基礎: HTTP/1.1, HTTP/2, HTTP/3 の違い、キャッシュヘッダー、リクエスト/レスポンスの流れを知っていること

この章で学ぶこと

  • ネットワークパフォーマンスのボトルネックを理解する
  • レイテンシ削減と帯域最適化の手法を把握する
  • 接続管理とリソースの最適化戦略を学ぶ
  • HTTP/2・HTTP/3 による多重化と高速接続を理解する
  • キャッシュ戦略とService Workerを活用したオフライン対応を学ぶ
  • Core Web Vitals の計測と改善手法を実践する
  • CDN設計とエッジコンピューティングの活用を把握する

1. パフォーマンスのボトルネック

1.1 Webページ読み込みの時間内訳

Webページ読み込みの時間内訳:

  DNS解決:        ~50ms
  TCP接続:        ~30ms(1.5 RTT)
  TLSハンドシェイク: ~50ms(1-2 RTT)
  リクエスト送信:  ~5ms
  サーバー処理:    ~100ms(TTFB)
  コンテンツ転送:  ~200ms
  レンダリング:    ~300ms
  ─────────────────────────
  合計:            ~735ms

主要なボトルネック:
  ① レイテンシ(往復時間):
     → 物理的距離に依存(光速の限界)
     → RTT × ラウンドトリップ回数

  ② 帯域幅:
     → 大きなファイルの転送時間
     → 画像、動画、JavaScriptバンドル

  ③ サーバー処理時間:
     → DB クエリ、API呼び出し、計算処理

  ④ レンダリング:
     → DOM構築、CSS計算、JavaScript実行
     → レンダリングブロッキング リソース

1.2 ネットワークウォーターフォールの詳細分析

典型的なWebページのウォーターフォール分析:

  リクエスト1: HTML (index.html)
  ├── DNS  │ TCP │ TLS │ TTFB │ Download
  │   50ms │30ms │50ms │100ms │ 20ms     = 250ms
  │
  ├── リクエスト2: CSS (styles.css) - レンダリングブロック
  │   ├── TTFB │ Download
  │   │   30ms │ 40ms     = 70ms (接続再利用)
  │   │
  │   ├── リクエスト3: JS (app.js) - パーサーブロック
  │   │   ├── TTFB │ Download │ Parse │ Execute
  │   │   │   30ms │ 100ms   │ 50ms  │ 200ms  = 380ms
  │   │   │
  │   │   ├── リクエスト4: API (GET /api/data) - JSから発火
  │   │   │   ├── DNS │ TCP │ TLS │ TTFB │ Download
  │   │   │   │   50ms│30ms│50ms │150ms │ 30ms   = 310ms
  │   │   │   │
  │   │   │   └── リクエスト5: 画像 (hero.webp) - APIデータ後に表示
  │   │   │       ├── TTFB │ Download
  │   │   │       │   20ms │ 80ms     = 100ms
  │
  合計クリティカルパス: 250 + 70 + 380 + 310 + 100 = 1,110ms

  最適化後のクリティカルパス:
  ├── preconnect: API の DNS+TCP+TLS を事前解決 (-130ms)
  ├── preload: CSS と JS を並列取得 (-70ms)
  ├── async/defer: JS のパーサーブロック解除 (-50ms)
  ├── SSR: API 呼び出し不要 (-310ms)
  ├── priority hints: hero画像を優先ロード
  └── 結果: 1,110ms → ~550ms(50%削減)

1.3 ブラウザの同時接続数制限

ブラウザの同時接続数制限:

  HTTP/1.1:
ブラウザ同時接続数/ホスト
Chrome6
Firefox6
Safari6
Edge6
→ 7番目以降のリクエストはキューで待機
  → ドメインシャーディング: img1.example.com, img2.example.com
    → HTTP/1.1 時代のワークアラウンド(非推奨)

  HTTP/2:
  → 1つの接続で多数のストリームを多重化
  → 同時接続数制限は実質的に無制限
  → ドメインシャーディングは逆効果(接続確立コスト)

  HTTP/3:
  → QUICベースで接続確立が高速
  → Head-of-Line Blocking が解消
  → パケットロス時のパフォーマンスが向上

1.4 クリティカルレンダリングパス

クリティカルレンダリングパス(CRP):

  HTML → DOM Tree
    ↓
  CSS → CSSOM Tree
    ↓
  DOM + CSSOM → Render Tree
    ↓
  Layout(レイアウト計算)
    ↓
  Paint(画面描画)
    ↓
  Composite(合成)

  レンダリングブロッキングリソース:
  ① CSS(全てのCSSがパース完了するまでレンダリングしない)
     対策: Critical CSS のインライン化
           非クリティカルCSSの非同期読み込み

  ② JavaScript(script タグがDOMパースをブロック)
     対策: async / defer 属性
           script タグを body 末尾に配置
           dynamic import

  Critical CSS の例:
  <!-- head 内にインライン化 -->
  <style>
    /* ファーストビューに必要な最小限のCSS */
    body { margin: 0; font-family: sans-serif; }
    .hero { height: 100vh; display: flex; align-items: center; }
    .nav { position: fixed; top: 0; width: 100%; }
  </style>

  <!-- 残りのCSSは非同期ロード -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>

2. レイテンシ削減

2.1 CDN の活用

CDN(Content Delivery Network)の活用:

  基本原理:
  → ユーザーに近いエッジサーバーから配信
  → RTT: 100ms → 5ms(東京のユーザーが東京エッジから取得)
  → オリジンサーバーへの負荷軽減

  CDNの階層構造:
オリジンサーバー (us-east-1)
PoPPoPPoP
│          │              │
   ユーザーA   ユーザーB      ユーザーC

  主要CDNプロバイダの比較:
プロバイダエッジ数特徴
CloudFront400+AWS統合、Lambda@Edge
Cloudflare300+無料プラン、Workers
Fastly90+VCL、リアルタイムパージ
Akamai4,000+最大規模、エンタープライズ
Vercel Edge自動Next.js最適化

2.2 CDN キャッシュ戦略

# CDNキャッシュ戦略の設計
 
# 1. 静的アセット(変更時はファイル名が変わる: contenthash)
# /assets/app.a1b2c3.js
location ~* \.(js|css|woff2|png|jpg|webp|avif|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    # immutable: ブラウザが条件付きリクエストも送らない
    # 1年間キャッシュ(ファイル名にハッシュが含まれるため安全)
}
 
# 2. HTML(常に最新チェック)
location ~* \.html$ {
    add_header Cache-Control "public, no-cache";
    # no-cache: 毎回サーバーに検証(ETagで304応答)
    # → HTMLが最新のアセットURLを参照するため必要
}
 
# 3. API レスポンス(短時間キャッシュ + SWR)
location /api/ {
    add_header Cache-Control "public, max-age=10, stale-while-revalidate=60";
    # 10秒間はキャッシュ使用
    # 10-70秒: staleキャッシュを返しつつバックグラウンドで再検証
}
 
# 4. ユーザー固有データ(キャッシュ禁止)
location /api/me {
    add_header Cache-Control "private, no-store";
    # CDNにキャッシュさせない
}
CloudFront のキャッシュ動作:

  リクエストフロー:
  1. ユーザー → エッジ: GET /api/products
  2. エッジ: キャッシュにあるか?
     ├── HIT: キャッシュからレスポンス(<1ms)
     ├── MISS: オリジンにリクエスト
     │   → レスポンスをキャッシュ + ユーザーに返却
     └── STALE: 古いキャッシュを返しつつ再検証

  キャッシュヒット率の目標:
  静的アセット: > 95%
  API: > 70%(コンテンツによる)
  HTML: > 50%

  キャッシュキーの設計:
  → URL + Query String + Accept-Encoding + Accept(画像形式)
  → 不要なヘッダーやCookieをキャッシュキーから除外
  → Vary ヘッダーの適切な設定

2.3 接続の事前確立

<!-- 接続の事前確立 -->
 
<!-- 1. dns-prefetch: DNSだけ事前解決(軽量) -->
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
 
<!-- 2. preconnect: DNS + TCP + TLS を事前確立(推奨) -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- crossorigin: CORS リクエストの場合に必要 -->
 
<!-- 3. preload: リソースを優先ダウンロード -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero.webp" as="image">
 
<!-- 4. prefetch: 次のページで必要なリソースを事前取得(低優先度) -->
<link rel="prefetch" href="/next-page.html">
<link rel="prefetch" href="/data/products.json">
 
<!-- 5. prerender: 次のページを事前レンダリング(Chrome) -->
<link rel="prerender" href="/likely-next-page">
 
<!-- 6. modulepreload: ESモジュールの事前ロード -->
<link rel="modulepreload" href="/modules/app.js">
リソースヒントの効果測定:

  preconnect の効果:
操作preconnectなし
DNS解決事前完了50ms
TCP接続事前完了30ms
TLSハンドシェイク事前完了50ms
最初のリクエスト即座+130ms
preconnect は最大3-4個のドメインに限定する:
  → 過剰な preconnect は CPU/メモリを消費
  → 最も重要な外部ドメインのみに使用

2.4 HTTP/2 と HTTP/3

HTTP/2 の最適化:

  主要機能:
  ① 多重化(Multiplexing):
     → 1つのTCP接続で複数のストリームを並行処理
     → リクエスト/レスポンスの順序に依存しない
     → HTTP/1.1 の Head-of-Line Blocking を解消

  ② ヘッダー圧縮(HPACK):
     → 静的テーブル + 動的テーブル
     → 繰り返しのヘッダーは1バイトで参照
     → Cookie等の大きなヘッダーの圧縮に効果大

  ③ サーバープッシュ:
     → サーバーがクライアントの要求前にリソースを送信
     → HTML を返す際に CSS/JS も一緒にプッシュ
     → 注意: ブラウザキャッシュとの競合で非推奨の方向
     → 代替: 103 Early Hints

  ④ ストリーム優先度:
     → CSS > JS > 画像 の優先順位を設定
     → ブラウザが自動的に最適な優先度を設定

HTTP/3 (QUIC) の利点:

  HTTP/2 の問題(TCP Head-of-Line Blocking):
  → TCPレベルでパケットロスが発生すると全ストリームが停止
  → HTTP/2 でも TCP の制約は回避不可能

  HTTP/3 の解決策:
  → QUIC(UDP上のトランスポートプロトコル)を使用
  → ストリーム単位での独立した制御
  → 1つのストリームのパケットロスが他に影響しない

  接続確立の高速化:
プロトコルRTT0-RTT再接続
HTTP/1.1+TLS3 RTTN/A
HTTP/2+TLS2 RTTN/A
HTTP/3+QUIC1 RTT0 RTT(再接続時)
コネクションマイグレーション:
  → Wi-Fi → モバイル回線 切り替え時も接続維持
  → Connection ID で接続を識別(IPアドレスに依存しない)
# Nginx でのHTTP/2設定
server {
    listen 443 ssl http2;
    server_name example.com;
 
    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;
 
    # ALPN(Application-Layer Protocol Negotiation)
    ssl_protocols TLSv1.2 TLSv1.3;
 
    # HTTP/2 サーバープッシュ(非推奨だが参考)
    # location / {
    #     http2_push /styles.css;
    #     http2_push /app.js;
    # }
 
    # 103 Early Hints(代替手段)
    location / {
        add_header Link "</styles.css>; rel=preload; as=style" always;
        add_header Link "</app.js>; rel=preload; as=script" always;
    }
}
 
# Nginx でのHTTP/3設定 (nginx-quic)
server {
    listen 443 quic reuseport;
    listen 443 ssl http2;
 
    ssl_protocols TLSv1.3;  # HTTP/3 は TLS 1.3 必須
 
    add_header Alt-Svc 'h3=":443"; ma=86400';
    # Alt-Svc: ブラウザに HTTP/3 対応を通知
}

2.5 接続の再利用と接続プーリング

接続の再利用:

  HTTP/1.1 Keep-Alive:
  → Connection: keep-alive(デフォルト)
  → TCP + TLS のハンドシェイクコストを回避
  → Keep-Alive-Timeout でアイドル時間を設定

  Nginx 設定:
  keepalive_timeout 65;        # クライアント接続のタイムアウト
  keepalive_requests 1000;     # 1接続あたりの最大リクエスト数

  upstream backend {
      server app:3000;
      keepalive 32;             # バックエンドへのKeep-Alive接続プール
      keepalive_requests 100;
      keepalive_timeout 60s;
  }

  サーバー側の接続プーリング:
ブラウザ←────────────────→Nginx
│ Connection Pool
App(N)
DB Pool
Redis
データベース接続プーリング:
  → PgBouncer: PostgreSQL の接続プーリング
  → 接続確立: ~50ms → プールから取得: ~0.5ms
  → Transaction pooling: トランザクション単位で接続を再利用

2.6 サーバーの地理的分散

マルチリージョンデプロイ:

  構成例:
Route 53 / Cloudflare DNS
レイテンシベースルーティング
│             │             │
us-east-1eu-west-1ap-northeast
VirginiaIrelandTokyo
App ServerApp ServerApp Server
Read DBRead DBRead DB
│             │             │
             └─────────────┼─────────────┘
Primary DB
(us-east-1)
レイテンシ比較(東京ユーザー):
サーバー位置RTT
東京~5ms
シンガポール~70ms
バージニア~170ms
ロンドン~250ms
データ同期戦略:
  → 最終整合性モデル(Eventually Consistent)
  → CRDTs(Conflict-free Replicated Data Types)
  → リードレプリカ + ライトリーダーパターン

3. 帯域最適化

3.1 テキスト圧縮

テキスト圧縮の比較:

  圧縮アルゴリズム:
方式圧縮率圧縮速度サポート
gzip70-80%高速全ブラウザ
Brotli80-90%中速全モダンブラウザ
zstd75-85%最高速Chrome 123+
実測値(React アプリの main.js: 500KB):
方式圧縮後サイズ削減率
未圧縮500KB-
gzip145KB71%
Brotli120KB76%
zstd130KB74%
# Nginx でのBrotli圧縮設定
# ngx_brotli モジュールが必要
 
# 動的圧縮(リクエスト時に圧縮)
brotli on;
brotli_comp_level 6;    # 1-11(6が速度と圧縮率のバランス)
brotli_types
    text/plain
    text/css
    text/javascript
    application/json
    application/javascript
    application/x-javascript
    application/xml
    image/svg+xml;
brotli_min_length 1024;  # 1KB未満は圧縮しない
 
# 静的事前圧縮(ビルド時に .br ファイルを生成)
brotli_static on;
# → app.js.br が存在すれば、動的圧縮せずそのまま返す
# → ビルド時に brotli app.js で生成
 
# gzip フォールバック
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;
gzip_static on;
// Vite でのビルド時圧縮設定
// vite.config.ts
import { defineConfig } from 'vite';
import viteCompression from 'vite-plugin-compression';
 
export default defineConfig({
  plugins: [
    // Brotli 圧縮
    viteCompression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 1024,     // 1KB以上のファイルを圧縮
    }),
    // gzip フォールバック
    viteCompression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 1024,
    }),
  ],
  build: {
    // ソースマップはプロダクションでは無効化
    sourcemap: false,
    // チャンクサイズ警告の閾値
    chunkSizeWarningLimit: 500,
    rollupOptions: {
      output: {
        // ベンダーチャンクの分離
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
        },
      },
    },
  },
});

3.2 画像最適化

画像フォーマットの選択:

  フォーマット比較(同品質のベンチマーク):
形式圧縮率用途ブラウザ対応
AVIFJPEG比50%↓写真(次世代、推奨)Chrome/FF/Sf
WebPJPEG比30%↓写真(推奨)全モダン
JPEG XLJPEG比35%↓写真(実験的)限定的
SVG極小アイコン、ロゴ全ブラウザ
PNG大きい透過が必要な場合のみ全ブラウザ
JPEGベースラインフォールバック全ブラウザ
実測値(写真 1920x1080px, 品質80):
形式ファイルサイズ
PNG3.2MB
JPEG280KB
WebP195KB
AVIF140KB
<!-- レスポンシブ画像の完全実装 -->
 
<!-- 1. 基本的なpicture要素 -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg"
       alt="商品画像"
       loading="lazy"
       decoding="async"
       width="800"
       height="600">
</picture>
 
<!-- 2. レスポンシブ対応(解像度とビューポート幅) -->
<picture>
  <!-- デスクトップ向け -->
  <source
    media="(min-width: 1024px)"
    srcset="hero-1600.avif 1600w, hero-1200.avif 1200w"
    sizes="100vw"
    type="image/avif">
  <source
    media="(min-width: 1024px)"
    srcset="hero-1600.webp 1600w, hero-1200.webp 1200w"
    sizes="100vw"
    type="image/webp">
 
  <!-- モバイル向け -->
  <source
    srcset="hero-800.avif 800w, hero-400.avif 400w"
    sizes="100vw"
    type="image/avif">
  <source
    srcset="hero-800.webp 800w, hero-400.webp 400w"
    sizes="100vw"
    type="image/webp">
 
  <img
    src="hero-800.jpg"
    alt="ヒーローイメージ"
    loading="eager"
    fetchpriority="high"
    width="1600"
    height="900">
</picture>
 
<!-- 3. Next.js Image コンポーネント(自動最適化) -->
<!-- 上記の複雑さをフレームワークが吸収 -->
// Next.js Image コンポーネントの活用
import Image from 'next/image';
 
// 基本使用
function ProductCard({ product }: { product: Product }) {
  return (
    <div>
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={400}
        height={300}
        // 自動的に WebP/AVIF 変換
        // レスポンシブ srcset 生成
        // lazy loading デフォルト
        placeholder="blur"
        blurDataURL={product.blurHash}
      />
    </div>
  );
}
 
// ヒーロー画像(LCP対象)
function HeroImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="メインビジュアル"
      fill                   // 親要素を満たす
      sizes="100vw"
      priority               // lazy loading 無効、preload ヒント追加
      quality={85}
      className="object-cover"
    />
  );
}
 
// next.config.js での画像最適化設定
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 365, // 1年
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        pathname: '/uploads/**',
      },
    ],
  },
};

3.3 JavaScript の最適化

// コード分割(Code Splitting)
 
// 1. ルートベースの分割(React Router)
import { lazy, Suspense } from 'react';
 
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import(
  /* webpackChunkName: "analytics" */
  /* webpackPrefetch: true */
  './pages/Analytics'
));
 
function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}
 
// 2. コンポーネントベースの分割
const HeavyEditor = lazy(() => import('./components/HeavyEditor'));
// → HeavyEditor は必要な時だけロード
 
function EditorPage() {
  const [showEditor, setShowEditor] = useState(false);
  return (
    <div>
      <button onClick={() => setShowEditor(true)}>Open Editor</button>
      {showEditor && (
        <Suspense fallback={<EditorSkeleton />}>
          <HeavyEditor />
        </Suspense>
      )}
    </div>
  );
}
 
// 3. 条件付きインポート
async function processImage(file: File) {
  // sharp は使用時にのみロード
  const { processWithSharp } = await import('./utils/imageProcessor');
  return processWithSharp(file);
}
 
// 4. Tree Shaking の効果的な活用
// 悪い(バンドル全体がインポートされる)
import _ from 'lodash';
_.debounce(fn, 300);
 
// 良い(必要な関数のみ)
import debounce from 'lodash/debounce';
debounce(fn, 300);
 
// 最良(ESModules で Tree Shaking 可能)
import { debounce } from 'lodash-es';
debounce(fn, 300);
バンドル分析と最適化:

  目標サイズ:
カテゴリgzip後の目標サイズ
初期JSバンドル(合計)< 200KB
フレームワーク(React等)< 45KB
ルートごとのチャンク< 100KB
サードパーティ合計< 100KB
分析ツール:
  → webpack-bundle-analyzer:
    npx webpack-bundle-analyzer stats.json

  → source-map-explorer:
    npx source-map-explorer build/static/js/*.js

  → Vite のビルドレポート:
    npx vite-bundle-visualizer

  大きなライブラリの代替:
ライブラリサイズ代替サイズ
moment72KBday.js2KB
lodash71KBlodash-esTree可
chart.js63KBlightweight-15KB
uuid12KBnanoid0.5KB
axios13KBfetch API0KB

3.4 フォント最適化

/* フォント最適化の実装 */
 
/* 1. font-display: swap でテキストを先に表示 */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-v13-latin-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;  /* FOUT: 無スタイルテキストを先に表示 */
}
 
/* font-display の選択肢:
   auto:     ブラウザ任せ
   block:    3秒間非表示 → フォント表示(FOIT)
   swap:     即フォールバック → フォント切替(FOUT)- 推奨
   fallback: 100ms非表示 → フォールバック → 3秒以内に切替
   optional: 即フォールバック → 十分高速ならフォント使用
*/
 
/* 2. 日本語フォントのサブセット化 */
/* Noto Sans JP: フルセット = 5.7MB → サブセット = 200KB */
@font-face {
  font-family: 'Noto Sans JP';
  /* unicode-range でサブセットを定義 */
  src: url('/fonts/NotoSansJP-Regular-subset.woff2') format('woff2');
  unicode-range: U+3000-303F, U+3040-309F, U+30A0-30FF,
                 U+4E00-9FFF, U+FF00-FFEF;
  font-display: swap;
}
 
/* 3. システムフォントスタック(ゼロコスト) */
body {
  font-family:
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    'Helvetica Neue',
    Arial,
    'Noto Sans',
    'Noto Sans JP',
    sans-serif;
}
<!-- フォントのpreload -->
<link rel="preload"
      href="/fonts/inter-v13-latin-regular.woff2"
      as="font"
      type="font/woff2"
      crossorigin>
 
<!-- Google Fonts の最適な読み込み -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
      rel="stylesheet">
 
<!-- Next.js の next/font(最適化済み) -->
<!-- ビルド時にフォントをダウンロードしてセルフホスティング -->
// Next.js next/font の使用
import { Inter, Noto_Sans_JP } from 'next/font/google';
 
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});
 
const notoSansJP = Noto_Sans_JP({
  subsets: ['latin'],
  weight: ['400', '700'],
  display: 'swap',
  variable: '--font-noto-sans-jp',
  preload: false, // 日本語フォントは大きいのでpreloadしない
});
 
export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${notoSansJP.variable}`}>
      <body>{children}</body>
    </html>
  );
}

3.5 動画とメディアの最適化

<!-- 動画の最適化 -->
 
<!-- 1. 遅延読み込み + 適応的品質 -->
<video
  poster="/video-poster.webp"
  preload="none"
  playsinline
  muted
  loop>
  <!-- 低帯域ユーザー向けに解像度を段階的に提供 -->
  <source src="/video-720p.mp4" type="video/mp4"
          media="(max-width: 768px)">
  <source src="/video-1080p.mp4" type="video/mp4">
</video>
 
<!-- 2. Intersection Observer で遅延再生 -->
<script>
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const video = entry.target;
    if (entry.isIntersecting) {
      video.play();
    } else {
      video.pause();
    }
  });
}, { threshold: 0.5 });
 
document.querySelectorAll('video[data-lazy]').forEach(v => observer.observe(v));
</script>
メディア最適化のチェックリスト:

  画像:
  □ WebP/AVIF 形式を使用
  □ レスポンシブ srcset + sizes を設定
  □ width/height を明示(CLS 防止)
  □ loading="lazy" をファーストビュー以外に設定
  □ LCP 画像に fetchpriority="high" を設定
  □ placeholder/blurHash を使用

  動画:
  □ poster 画像を設定
  □ preload="none" でデータ節約
  □ 適応的ビットレート(HLS/DASH)
  □ ファーストビュー外は Intersection Observer で制御

  SVG:
  □ SVGO で最適化(不要メタデータ除去)
  □ 小さなSVGはインライン化
  □ スプライトシートの活用(アイコン)

4. API最適化

4.1 レスポンスの最小化

// REST API でのフィールド選択
// GET /api/users?fields=id,name,email
 
// サーバー側実装(Express + Prisma)
app.get('/api/users', async (req, res) => {
  const fields = req.query.fields?.split(',') || [];
 
  const select = fields.length > 0
    ? Object.fromEntries(fields.map(f => [f, true]))
    : undefined; // 未指定なら全フィールド
 
  const users = await prisma.user.findMany({
    select,
    take: 20,
  });
 
  res.json({ data: users });
});
 
// GraphQL でのフィールド選択(自動最適化)
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      # avatar や profile は不要なので取得しない
    }
  }
`;
 
// tRPC でのフィールド選択
const users = await trpc.user.list.query({
  select: { id: true, name: true, email: true },
});

4.2 バッチリクエストとデータローダー

// バッチリクエスト: 複数APIコールを1リクエストに
 
// クライアント側
const batchRequest = async (requests: BatchItem[]) => {
  const response = await fetch('/api/batch', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ requests }),
  });
  return response.json();
};
 
// 使用例: 3つのAPIを1リクエストに
const results = await batchRequest([
  { method: 'GET', url: '/api/users/1' },
  { method: 'GET', url: '/api/users/1/orders' },
  { method: 'GET', url: '/api/users/1/notifications' },
]);
 
// サーバー側: DataLoader パターン(N+1防止)
import DataLoader from 'dataloader';
 
// ユーザーのバッチ取得
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
  const users = await prisma.user.findMany({
    where: { id: { in: [...userIds] } },
  });
  // IDの順序を保持して返す
  const userMap = new Map(users.map(u => [u.id, u]));
  return userIds.map(id => userMap.get(id) || null);
});
 
// 使用(個別に呼んでもバッチ実行される)
const user1 = await userLoader.load('user-1');
const user2 = await userLoader.load('user-2');
// → 1つのSQLクエリにバッチ化: SELECT * FROM users WHERE id IN ('user-1', 'user-2')

4.3 ページネーション

// Cursor-based ページネーション(推奨)
 
// メリット:
// → 一貫した結果(途中のデータ追加/削除に影響されない)
// → 大量データでもパフォーマンス安定(OFFSET不要)
// → 無限スクロールに最適
 
// API 設計
// GET /api/posts?cursor=abc123&limit=20
 
// サーバー側(Prisma)
async function getPosts(cursor?: string, limit = 20) {
  const posts = await prisma.post.findMany({
    take: limit + 1, // 1件多く取得して次ページ有無を判定
    ...(cursor && {
      cursor: { id: cursor },
      skip: 1, // cursorの要素自体はスキップ
    }),
    orderBy: { createdAt: 'desc' },
  });
 
  const hasMore = posts.length > limit;
  const data = hasMore ? posts.slice(0, limit) : posts;
  const nextCursor = hasMore ? data[data.length - 1].id : null;
 
  return {
    data,
    nextCursor,
    hasMore,
  };
}
 
// レスポンス例
{
  "data": [
    { "id": "post-20", "title": "..." },
    { "id": "post-19", "title": "..." }
  ],
  "nextCursor": "post-19",
  "hasMore": true
}
 
// クライアント側(TanStack Query useInfiniteQuery)
function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/posts?cursor=${pageParam || ''}&limit=20`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: '',
  });
}

4.4 キャッシュ戦略の詳細

HTTPキャッシュの全体像:

  ブラウザキャッシュ → CDNキャッシュ → オリジンサーバー

  Cache-Control ヘッダーの解説:
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
public: CDN/共有キャッシュに保存可
private: ブラウザキャッシュのみ(ユーザー固有データ)
max-age=3600: 3600秒(1時間)キャッシュ有効
s-maxage=3600: CDNのみに適用される max-age
no-cache: 毎回サーバーに確認(ETag/Last-Modified)
no-store: 一切キャッシュしない
immutable: max-age 内は条件付きリクエストも不要
stale-while-revalidate=86400:
max-age 超過後、86400秒間は古いキャッシュを返しつつ再検証
stale-if-error=86400:
オリジンエラー時、86400秒間は古いキャッシュを返す
ETag + 条件付きリクエスト:
  1. レスポンス: ETag: "abc123"
  2. 次のリクエスト: If-None-Match: "abc123"
  3. 変更なし → 304 Not Modified(ボディなし、帯域節約)
  4. 変更あり → 200 OK(新しいデータ)

  キャッシュ無効化(Cache Busting):
  → ファイル名にコンテンツハッシュ: app.a1b2c3d4.js
  → クエリパラメータ: styles.css?v=20240101(非推奨: CDNで効かない場合あり)
// アプリケーションレベルのキャッシュ戦略
 
// 1. TanStack Query のキャッシュ設定
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,    // 5分間はfreshとみなす
      gcTime: 30 * 60 * 1000,      // 30分間メモリに保持
      refetchOnWindowFocus: true,   // フォーカス復帰時に再取得
      refetchOnReconnect: true,     // ネットワーク復帰時に再取得
      retry: 3,                     // 3回リトライ
      retryDelay: (attempt) =>      // エクスポネンシャルバックオフ
        Math.min(1000 * 2 ** attempt, 30000),
    },
  },
});
 
// 2. リソース種類ごとのキャッシュ設定
const userQuery = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 10 * 60 * 1000,       // ユーザー情報: 10分
  gcTime: 60 * 60 * 1000,          // 1時間メモリ保持
});
 
const notificationsQuery = useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  staleTime: 30 * 1000,             // 通知: 30秒(頻繁に更新)
  refetchInterval: 60 * 1000,       // 60秒ごとにポーリング
});
 
// 3. Service Worker キャッシュ(Workbox)
// Cache First: 静的アセット
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30日
      }),
    ],
  })
);
 
// Stale While Revalidate: API
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    cacheName: 'api-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60, // 5分
      }),
    ],
  })
);
 
// Network First: HTML
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 20,
      }),
    ],
  })
);

4.5 データ圧縮とシリアライゼーション

// JSON の効率的なシリアライゼーション
 
// 1. 不要なフィールドの除外
function serializeUser(user: User) {
  const { password, internalNotes, ...publicData } = user;
  return publicData;
}
 
// 2. superjson: Date, Map, Set などのシリアライゼーション
import superjson from 'superjson';
 
// サーバー
const data = { createdAt: new Date(), tags: new Set(['a', 'b']) };
const serialized = superjson.stringify(data);
 
// クライアント
const parsed = superjson.parse(serialized);
// parsed.createdAt は Date オブジェクト
// parsed.tags は Set オブジェクト
 
// 3. Protocol Buffers(高性能API向け)
// → JSON の 3-10倍高速なシリアライゼーション
// → ペイロードが 50-80% 小さい
// → gRPC, Connect で使用
 
// user.proto
// message User {
//   string id = 1;
//   string name = 2;
//   string email = 3;
//   google.protobuf.Timestamp created_at = 4;
// }
 
// 4. MessagePack(バイナリJSON)
// → JSONと同等のデータ構造をバイナリで表現
// → JSON比 20-30% 小さい
// → パース速度が 2-5倍

4.6 接続プーリングとコネクション管理

// データベース接続プーリング
 
// 1. Prisma の接続プーリング
// prisma/schema.prisma
// datasource db {
//   provider = "postgresql"
//   url      = env("DATABASE_URL")
//   // ?connection_limit=10&pool_timeout=30
// }
 
// 2. PgBouncer の設定
// pgbouncer.ini
// [databases]
// mydb = host=localhost port=5432 dbname=mydb
//
// [pgbouncer]
// pool_mode = transaction        # トランザクション単位で接続を再利用
// max_client_conn = 1000         # 最大クライアント接続
// default_pool_size = 20         # プールサイズ
// min_pool_size = 5              # 最小プールサイズ
// reserve_pool_size = 5          # 予備プール
 
// 3. Redis 接続プーリング(ioredis)
import Redis from 'ioredis';
 
const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: 6379,
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
  lazyConnect: true,
  // Cluster mode
  // sentinels: [{ host: 'sentinel1', port: 26379 }],
});
 
// 4. HTTP クライアントの接続プーリング
import { Agent } from 'undici';
 
const agent = new Agent({
  connections: 100,         // 最大接続数
  pipelining: 10,          // パイプライニング数
  keepAliveTimeout: 60000, // Keep-Alive タイムアウト
  keepAliveMaxTimeout: 600000,
});
 
const response = await fetch('https://api.example.com/data', {
  dispatcher: agent,
});

5. Web Vitals とパフォーマンス計測

5.1 Core Web Vitals の詳細

Core Web Vitals:
指標測定内容GoodPoor
LCP最大コンテンツの表示< 2.5s> 4.0s
INPインタラクション遅延< 200ms> 500ms
CLSレイアウトのずれ< 0.1> 0.25
LCP(Largest Contentful Paint):
  → ビューポート内の最大要素が表示されるまでの時間
  → 対象: <img>, <video>, background-image, テキストブロック
  → 改善策:
     ① リソースのプリロード: <link rel="preload">
     ② 画像の最適化: WebP/AVIF, srcset
     ③ サーバーレスポンスの高速化: TTFB < 800ms
     ④ レンダリングブロックの排除: async/defer
     ⑤ fetchpriority="high" の設定

  INP(Interaction to Next Paint):
  → ユーザーの操作から次の描画更新までの時間
  → FID の後継指標(2024年3月〜)
  → 対象: click, tap, keypress
  → 改善策:
     ① 長いタスクの分割: requestIdleCallback, setTimeout
     ② イベントハンドラの最適化
     ③ 不要な再レンダリングの防止: React.memo, useMemo
     ④ Web Worker での重い処理のオフロード
     ⑤ Concurrent Features の活用: useTransition, useDeferredValue

  CLS(Cumulative Layout Shift):
  → ページ読み込み中のレイアウトのずれの累積スコア
  → 改善策:
     ① img/video に width/height を明示
     ② font-display: swap + サイズ一致フォールバック
     ③ 広告・埋め込みコンテンツの領域を事前確保
     ④ 動的コンテンツの挿入位置を工夫
     ⑤ CSS containment の活用

5.2 補助指標

ネットワーク関連の補助指標:

  TTFB(Time to First Byte): < 800ms
  → サーバーレスポンスの速度
  → DNS + TCP + TLS + サーバー処理時間
  → 改善: CDN, キャッシュ, サーバー最適化

  FCP(First Contentful Paint): < 1.8s
  → 最初のコンテンツ表示
  → テキスト、画像、SVG、非白色 canvas
  → 改善: Critical CSS, preload, SSR

  TTFB → FCP の差分:
  → レンダリングブロッキングの影響
  → 大きな差分 = CSSやJSがブロックしている

  TBT(Total Blocking Time):
  → FCP と TTI の間のメインスレッドブロック時間の合計
  → 50ms超のタスクのうち50ms超過分の合計
  → INP と相関が高い

  Speed Index:
  → ページコンテンツの表示速度のスコア
  → ビジュアルの進捗を測定
  → 目標: < 3.4s

5.3 パフォーマンス計測の実装

// Performance API を使用した計測
 
// 1. Navigation Timing API
function measurePageLoad() {
  const [navigation] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
 
  const metrics = {
    // DNS 解決時間
    dns: navigation.domainLookupEnd - navigation.domainLookupStart,
    // TCP 接続時間
    tcp: navigation.connectEnd - navigation.connectStart,
    // TLS ハンドシェイク時間
    tls: navigation.secureConnectionStart > 0
      ? navigation.connectEnd - navigation.secureConnectionStart
      : 0,
    // TTFB
    ttfb: navigation.responseStart - navigation.requestStart,
    // コンテンツダウンロード時間
    download: navigation.responseEnd - navigation.responseStart,
    // DOMContentLoaded
    domContentLoaded: navigation.domContentLoadedEventEnd - navigation.startTime,
    // Load イベント
    load: navigation.loadEventEnd - navigation.startTime,
    // DOM パース時間
    domParsing: navigation.domInteractive - navigation.responseEnd,
  };
 
  console.table(metrics);
  return metrics;
}
 
// 2. Resource Timing API
function measureResources() {
  const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
 
  // 最も遅いリソース TOP 10
  const slowest = resources
    .sort((a, b) => b.duration - a.duration)
    .slice(0, 10)
    .map(r => ({
      name: r.name.split('/').pop(),
      duration: Math.round(r.duration),
      size: r.transferSize,
      type: r.initiatorType,
      protocol: r.nextHopProtocol,
    }));
 
  console.table(slowest);
 
  // リソースタイプ別の合計
  const byType = resources.reduce((acc, r) => {
    const type = r.initiatorType;
    if (!acc[type]) acc[type] = { count: 0, totalSize: 0, totalDuration: 0 };
    acc[type].count++;
    acc[type].totalSize += r.transferSize;
    acc[type].totalDuration += r.duration;
    return acc;
  }, {} as Record<string, { count: number; totalSize: number; totalDuration: number }>);
 
  console.table(byType);
}
 
// 3. Core Web Vitals の計測(web-vitals ライブラリ)
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
 
function reportWebVitals() {
  onLCP((metric) => {
    console.log('LCP:', metric.value, 'ms');
    console.log('LCP Element:', metric.entries[0]?.element);
    sendToAnalytics('LCP', metric);
  });
 
  onINP((metric) => {
    console.log('INP:', metric.value, 'ms');
    sendToAnalytics('INP', metric);
  });
 
  onCLS((metric) => {
    console.log('CLS:', metric.value);
    // CLS の各シフトの詳細
    metric.entries.forEach(entry => {
      console.log('Shift:', entry.value, entry.sources);
    });
    sendToAnalytics('CLS', metric);
  });
 
  onFCP((metric) => {
    console.log('FCP:', metric.value, 'ms');
    sendToAnalytics('FCP', metric);
  });
 
  onTTFB((metric) => {
    console.log('TTFB:', metric.value, 'ms');
    sendToAnalytics('TTFB', metric);
  });
}
 
// 4. カスタムパフォーマンスマーク
function measureCustom() {
  // APIコールの計測
  performance.mark('api-start');
 
  fetch('/api/users').then(async (res) => {
    performance.mark('api-end');
    performance.measure('api-call', 'api-start', 'api-end');
 
    const [measure] = performance.getEntriesByName('api-call');
    console.log('API call duration:', measure.duration, 'ms');
  });
}
 
// 5. Long Tasks API(INP改善用)
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.duration > 50) {
      console.warn('Long Task detected:', {
        duration: entry.duration,
        startTime: entry.startTime,
        name: entry.name,
      });
    }
  });
});
observer.observe({ type: 'longtask', buffered: true });

5.4 RUM(Real User Monitoring)

// RUM データの収集と送信
 
interface PerformanceData {
  url: string;
  userAgent: string;
  connectionType: string;
  effectiveType: string;
  lcp: number;
  inp: number;
  cls: number;
  fcp: number;
  ttfb: number;
  timestamp: number;
}
 
function collectPerformanceData(): void {
  const connection = (navigator as any).connection;
 
  const data: Partial<PerformanceData> = {
    url: window.location.href,
    userAgent: navigator.userAgent,
    connectionType: connection?.type || 'unknown',
    effectiveType: connection?.effectiveType || 'unknown',
    timestamp: Date.now(),
  };
 
  // web-vitals で各指標を収集
  onLCP(m => { data.lcp = m.value; maybeSend(data); });
  onINP(m => { data.inp = m.value; maybeSend(data); });
  onCLS(m => { data.cls = m.value; maybeSend(data); });
  onFCP(m => { data.fcp = m.value; maybeSend(data); });
  onTTFB(m => { data.ttfb = m.value; maybeSend(data); });
}
 
function maybeSend(data: Partial<PerformanceData>): void {
  // 全指標が揃ったら送信
  if (data.lcp && data.inp !== undefined && data.cls !== undefined) {
    sendBeacon(data);
  }
}
 
function sendBeacon(data: Partial<PerformanceData>): void {
  // sendBeacon: ページ遷移時でも確実に送信
  navigator.sendBeacon('/api/analytics/performance', JSON.stringify(data));
}
 
// RUM データの集約と分析(サーバー側)
// → p75 / p95 / p99 のパーセンタイルで分析
// → ネットワーク種別ごとの比較
// → ページ別の比較
// → リグレッション検出

5.5 計測ツールとダッシュボード

パフォーマンス計測ツール:

  ラボデータ(合成テスト):
ツール特徴
LighthouseChrome DevTools統合、スコア + 提案
WebPageTest世界各地からテスト、Waterfall分析
PageSpeed InsightsLighthouse + CrUXデータ
unlighthouseサイト全体を一括監査
フィールドデータ(実ユーザー):
ツール特徴
CrUXChrome実ユーザーデータ(無料)
Vercel AnalyticsNext.js向け、自動CWV計測
Sentryエラー + パフォーマンス計測
Datadog RUMエンタープライズ向け RUM
SpeedCurveRUM + 合成テスト + 可視化
CI/CD での自動計測:
  → Lighthouse CI: PR ごとにスコアを計測
  → Bundlesize: バンドルサイズの閾値チェック
  → web-vitals-reporter: CWV のリグレッション検出

  Lighthouse CI 設定例:
  // lighthouserc.js
  module.exports = {
    ci: {
      collect: {
        url: ['http://localhost:3000/', 'http://localhost:3000/products'],
        numberOfRuns: 3,
      },
      assert: {
        assertions: {
          'categories:performance': ['error', { minScore: 0.9 }],
          'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
          'interactive': ['error', { maxNumericValue: 3800 }],
          'cumulative-layout-shift': ['warn', { maxNumericValue: 0.1 }],
        },
      },
      upload: {
        target: 'temporary-public-storage',
      },
    },
  };

6. Service Worker とオフライン対応

6.1 Service Worker の基本

// Service Worker の登録
// app.ts
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/',
      });
      console.log('SW registered:', registration.scope);
    } catch (error) {
      console.error('SW registration failed:', error);
    }
  });
}
 
// sw.js - Service Worker
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/offline.html',
  '/styles.css',
  '/app.js',
  '/icons/icon-192x192.png',
];
 
// インストール: 静的アセットをキャッシュ
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
  self.skipWaiting(); // 即座にアクティブ化
});
 
// アクティベート: 古いキャッシュを削除
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
  self.clients.claim(); // 全クライアントを制御
});
 
// フェッチ: キャッシュ戦略の実装
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
 
  // API リクエスト: Network First
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
    return;
  }
 
  // 静的アセット: Cache First
  if (request.destination === 'image' ||
      request.destination === 'style' ||
      request.destination === 'script') {
    event.respondWith(cacheFirst(request));
    return;
  }
 
  // HTML: Network First with Offline Fallback
  if (request.mode === 'navigate') {
    event.respondWith(
      networkFirst(request).catch(() => caches.match('/offline.html'))
    );
    return;
  }
});
 
async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  const response = await fetch(request);
  const cache = await caches.open(CACHE_NAME);
  cache.put(request, response.clone());
  return response;
}
 
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch {
    return caches.match(request);
  }
}

6.2 Workbox を使った Service Worker

// Workbox: Google の Service Worker ライブラリ
 
// next.config.js (next-pwa)
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/api\.example\.com\/.*/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'api-cache',
        expiration: {
          maxEntries: 50,
          maxAgeSeconds: 60 * 60, // 1時間
        },
      },
    },
    {
      urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'image-cache',
        expiration: {
          maxEntries: 100,
          maxAgeSeconds: 30 * 24 * 60 * 60, // 30日
        },
      },
    },
    {
      urlPattern: /\.(?:js|css)$/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'static-cache',
        expiration: {
          maxEntries: 50,
          maxAgeSeconds: 24 * 60 * 60, // 1日
        },
      },
    },
  ],
});
 
module.exports = withPWA({
  // Next.js config
});

7. エッジコンピューティングとEdge Functions

7.1 エッジコンピューティングの概要

エッジコンピューティング:

  従来のアーキテクチャ:
  ユーザー → CDN(静的配信のみ) → オリジンサーバー(ロジック)

  エッジコンピューティング:
  ユーザー → エッジ(ロジック実行可能) → オリジン(必要時のみ)

  利点:
  → レイテンシ大幅削減(ユーザーの近くでロジック実行)
  → オリジンサーバーの負荷軽減
  → グローバルに分散した処理
  → コールドスタートが高速(数ms)

  制約:
  → 実行時間制限(通常 50ms〜30秒)
  → メモリ制限(128MB〜)
  → Node.js API の一部が使用不可
  → ステートレス(永続化はKV Store等で)
// Cloudflare Workers
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
 
    // A/Bテスト
    if (url.pathname === '/') {
      const bucket = request.headers.get('cf-connecting-ip')?.charCodeAt(0) % 2;
      const variant = bucket === 0 ? 'control' : 'experiment';
      const response = await fetch(`${url.origin}/variants/${variant}`);
      return new Response(response.body, {
        headers: {
          ...Object.fromEntries(response.headers),
          'X-Variant': variant,
        },
      });
    }
 
    // 地域別コンテンツ
    if (url.pathname === '/pricing') {
      const country = request.cf?.country || 'US';
      const currency = getCurrency(country);
      // KV Store からキャッシュされた料金を取得
      const pricing = await env.PRICING_KV.get(`pricing:${currency}`, 'json');
      return Response.json(pricing);
    }
 
    return fetch(request);
  },
};
 
// Vercel Edge Functions
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
export function middleware(request: NextRequest) {
  // 地域ベースのリダイレクト
  const country = request.geo?.country || 'US';
 
  if (country === 'JP' && !request.nextUrl.pathname.startsWith('/ja')) {
    return NextResponse.redirect(new URL('/ja' + request.nextUrl.pathname, request.url));
  }
 
  // レート制限
  const ip = request.ip || 'unknown';
  // Edge KV でレート制限を実装
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

7.2 エッジでのデータアクセス

// エッジデータベース
 
// 1. Cloudflare D1(エッジSQLite)
export default {
  async fetch(request: Request, env: Env) {
    const { results } = await env.DB
      .prepare('SELECT * FROM products WHERE category = ?')
      .bind('electronics')
      .all();
 
    return Response.json(results);
  },
};
 
// 2. Vercel KV(エッジRedis)
import { kv } from '@vercel/kv';
 
export async function GET(request: Request) {
  // セッション取得
  const session = await kv.get(`session:${sessionId}`);
 
  // レート制限
  const requests = await kv.incr(`ratelimit:${ip}`);
  if (requests === 1) {
    await kv.expire(`ratelimit:${ip}`, 60);
  }
  if (requests > 100) {
    return new Response('Too Many Requests', { status: 429 });
  }
 
  return Response.json({ data: session });
}
 
// 3. PlanetScale / Neon(エッジ対応DB)
import { neon } from '@neondatabase/serverless';
 
export async function GET(request: Request) {
  const sql = neon(process.env.DATABASE_URL!);
  const products = await sql`
    SELECT id, name, price FROM products
    WHERE category = 'electronics'
    ORDER BY created_at DESC
    LIMIT 20
  `;
  return Response.json(products);
}

8. ネットワークレジリエンス

8.1 リトライとバックオフ

// エクスポネンシャルバックオフ付きリトライ
async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries = 3,
  baseDelay = 1000,
): Promise<Response> {
  let lastError: Error;
 
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(10000), // 10秒タイムアウト
      });
 
      // 5xx エラーはリトライ対象
      if (response.status >= 500 && attempt < maxRetries) {
        throw new Error(`Server error: ${response.status}`);
      }
 
      // 429 Too Many Requests: Retry-After ヘッダーを参照
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
        await sleep(retryAfter * 1000);
        continue;
      }
 
      return response;
    } catch (error) {
      lastError = error as Error;
 
      if (attempt < maxRetries) {
        // エクスポネンシャルバックオフ + ジッター
        const delay = baseDelay * Math.pow(2, attempt);
        const jitter = delay * 0.1 * Math.random();
        console.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay + jitter}ms`);
        await sleep(delay + jitter);
      }
    }
  }
 
  throw lastError!;
}
 
function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}
 
// サーキットブレーカーパターン
class CircuitBreaker {
  private failures = 0;
  private lastFailure: number = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
 
  constructor(
    private threshold = 5,          // 失敗回数の閾値
    private resetTimeout = 30000,   // リセットまでの時間(ms)
  ) {}
 
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit is open');
      }
    }
 
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
 
  private onSuccess(): void {
    this.failures = 0;
    this.state = 'closed';
  }
 
  private onFailure(): void {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures >= this.threshold) {
      this.state = 'open';
    }
  }
}
 
// 使用例
const apiBreaker = new CircuitBreaker(5, 30000);
 
async function fetchUserData(userId: string) {
  return apiBreaker.execute(() =>
    fetchWithRetry(`/api/users/${userId}`)
  );
}

8.2 ネットワーク状態の検出

// ネットワーク状態の検出と適応
 
// 1. Online/Offline 検出
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
 
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
 
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
 
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
 
  return isOnline;
}
 
// 2. Network Information API
function useNetworkQuality() {
  const [quality, setQuality] = useState<'fast' | 'slow' | 'offline'>('fast');
 
  useEffect(() => {
    const connection = (navigator as any).connection;
    if (!connection) return;
 
    const updateQuality = () => {
      if (!navigator.onLine) {
        setQuality('offline');
        return;
      }
 
      const effectiveType = connection.effectiveType;
      // effectiveType: 'slow-2g', '2g', '3g', '4g'
      if (effectiveType === '4g' && connection.downlink > 5) {
        setQuality('fast');
      } else {
        setQuality('slow');
      }
    };
 
    connection.addEventListener('change', updateQuality);
    updateQuality();
 
    return () => connection.removeEventListener('change', updateQuality);
  }, []);
 
  return quality;
}
 
// 3. 適応的コンテンツ配信
function ProductImage({ product }: { product: Product }) {
  const quality = useNetworkQuality();
 
  return (
    <Image
      src={product.imageUrl}
      alt={product.name}
      quality={quality === 'fast' ? 85 : 50}  // 低速時は品質を下げる
      placeholder={quality === 'fast' ? 'blur' : 'empty'}
      loading={quality === 'fast' ? 'eager' : 'lazy'}
    />
  );
}
 
// 4. Save-Data ヘッダーの検出
// ユーザーがデータセーバーを有効にしている場合
function useSaveData() {
  const connection = (navigator as any).connection;
  return connection?.saveData === true;
}
 
// サーバー側での Save-Data 対応
// Save-Data: on ヘッダーを検出
// → 低品質画像を返す
// → 自動再生動画を無効化
// → 不要なアセットの読み込みをスキップ

9. パフォーマンス最適化チェックリスト

ネットワークパフォーマンス最適化チェックリスト:

  接続:
  □ CDNを使用している
  □ HTTP/2 以上を有効化している
  □ dns-prefetch / preconnect を設定している(最大3-4ドメイン)
  □ Keep-Alive が有効
  □ TLS 1.3 を使用している
  □ 103 Early Hints を検討している
  □ HTTP/3 (QUIC) のサポートを検討している

  転送:
  □ Brotli/gzip 圧縮を有効化している
  □ 画像をWebP/AVIF形式にしている
  □ レスポンシブ画像(srcset + sizes)を設定している
  □ LCP画像に fetchpriority="high" を設定している
  □ JavaScriptをコード分割している
  □ 初期バンドル < 200KB(gzip後)
  □ フォントをWOFF2 + サブセット化している
  □ font-display: swap を設定している
  □ Critical CSS をインライン化している
  □ 非クリティカルCSS/JSを async/defer にしている

  キャッシュ:
  □ 静的ファイルに Cache-Control + ハッシュ付きファイル名 + immutable
  □ HTMLに no-cache + ETag
  □ APIに stale-while-revalidate
  □ CDNキャッシュヒット率を監視している(目標 > 90%)
  □ Service Worker でオフライン対応

  API:
  □ 不要なフィールドを返していない
  □ ページネーション(Cursor-based推奨)を実装している
  □ N+1問題が発生していない(DataLoader使用)
  □ バッチリクエストを検討している
  □ 接続プーリングを使用している

  レンダリング:
  □ img/video に width/height を明示(CLS防止)
  □ 動的コンテンツの領域を事前確保している
  □ 長いタスクを分割している(INP改善)
  □ React.memo / useMemo で不要な再レンダリングを防止

  監視:
  □ Core Web Vitals (LCP, INP, CLS) を計測している
  □ TTFB を監視している(目標 < 800ms)
  □ エラー率を監視している
  □ RUM データを収集・分析している
  □ CI/CD で Lighthouse スコアを自動チェックしている
  □ バンドルサイズの推移を追跡している

  レジリエンス:
  □ リトライ + エクスポネンシャルバックオフを実装している
  □ タイムアウトを設定している
  □ サーキットブレーカーを検討している
  □ ネットワーク状態(Online/Offline)に対応している
  □ Save-Data ヘッダーに対応している

10. 実務での最適化フロー

10.1 パフォーマンスバジェット

パフォーマンスバジェットの設定:

  指標ベースのバジェット:
指標バジェット
LCP< 2.5s
INP< 200ms
CLS< 0.1
TTFB< 800ms
FCP< 1.8s
Lighthouse Performance> 90
リソースベースのバジェット:
リソースバジェット
初期JS (gzip)< 200KB
初期CSS (gzip)< 50KB
画像(ページあたり)< 1MB
フォント< 100KB
合計ページサイズ< 2MB
HTTPリクエスト数< 50
バジェット超過時の対応:
  → CI/CD で自動チェック(Lighthouse CI, bundlesize)
  → PR レビューで確認
  → 超過の場合: 別のリソースを削減 or 機能の見直し

10.2 段階的な最適化アプローチ

最適化の優先順位:

  Phase 1: Low Hanging Fruits(効果大 + 工数小)
  ① 圧縮の有効化(Brotli/gzip)
  ② 画像の最適化(WebP/AVIF変換)
  ③ Cache-Control の設定
  ④ preconnect / preload の設定
  ⑤ font-display: swap

  Phase 2: アーキテクチャ改善
  ⑥ コード分割(ルートベース + コンポーネントベース)
  ⑦ Tree Shaking + 大きなライブラリの置換
  ⑧ SSR / ISR の導入
  ⑨ Critical CSS のインライン化

  Phase 3: 高度な最適化
  ⑩ Service Worker + オフライン対応
  ⑪ Edge Functions / エッジコンピューティング
  ⑫ HTTP/3 の導入
  ⑬ RUM の導入と継続的モニタリング
  ⑭ A/Bテストによるパフォーマンス検証

  ROI の高い最適化:
  → 画像最適化: ページサイズの50%を占めることが多い
  → コード分割: 初期ロードの大幅削減
  → CDN + キャッシュ: TTFB の大幅改善
  → SSR: FCP / LCP の大幅改善

FAQ(よくある質問)

Q1: ネットワークパフォーマンス計測に最適なツールは?

A: 計測の目的とフェーズに応じてツールを使い分ける。

開発中のローカル計測:

ツール 用途 利点 欠点
Chrome DevTools フロントエンドの詳細分析 リアルタイム、視覚的、Waterfall 表示 ブラウザ依存、自動化困難
Lighthouse 総合的なパフォーマンス評価 Core Web Vitals, Best Practices, SEO も評価 シミュレーション(実ユーザー環境と乖離)
WebPageTest 多様な環境でのテスト 実機テスト、動画記録、比較機能 無料版は実行回数制限あり
curl API/バックエンドの計測 スクリプト化可能、TTFB 計測に最適 ブラウザの挙動を再現できない

CI/CD パイプラインでの自動計測:

# Lighthouse CI の例
npm install -g @lhci/cli
 
# lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['https://staging.example.com'],
      numberOfRuns: 5,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', {minScore: 0.9}],
        'first-contentful-paint': ['error', {maxNumericValue: 1800}],
        'largest-contentful-paint': ['error', {maxNumericValue: 2500}],
        'cumulative-layout-shift': ['error', {maxNumericValue: 0.1}],
      },
    },
  },
};
 
# CI で実行
lhci autorun

本番環境のリアルユーザーモニタリング(RUM):

ツール 特徴 コスト
Google Analytics 4 Core Web Vitals 自動収集、無料 無料
New Relic Browser 詳細な分析、エラー追跡 有料($99/月〜)
Datadog RUM インフラ監視と統合、セッションリプレイ 有料($1.50/1000セッション)
Sentry Performance エラー監視と統合、トランザクション追跡 無料枠あり

推奨アプローチ:

  1. 開発時: Chrome DevTools + Lighthouse
  2. CI/CD: Lighthouse CI で自動チェック
  3. 本番環境: RUM(GA4 or 有料サービス)で継続監視
  4. API: curl スクリプト + Grafana/Prometheus でメトリクス可視化

Q2: TCP 最適化のためのカーネルパラメータは?

A: Linux カーネルの TCP パラメータを調整することで、高スループット・低レイテンシを実現できる。

基本的な TCP パラメータ(/etc/sysctl.conf):

# ============================================
# TCP ウィンドウサイズの最適化
# ============================================
# TCP 受信バッファのサイズ(min, default, max)
net.ipv4.tcp_rmem = 4096 87380 67108864      # 最大 64MB
# TCP 送信バッファのサイズ(min, default, max)
net.ipv4.tcp_wmem = 4096 65536 67108864      # 最大 64MB
# ソケットバッファの最大サイズ
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
 
# ============================================
# TCP BBR 輻輳制御アルゴリズム(推奨)
# ============================================
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# BBR は Google が開発した最新の輻輳制御アルゴリズム
# 従来の CUBIC より高速・安定(特に高遅延・パケットロス環境)
 
# ============================================
# TCP Fast Open(3 ウェイハンドシェイクの短縮)
# ============================================
net.ipv4.tcp_fastopen = 3
# 3 = クライアントとサーバー両方で有効化
# 初回接続時に SYN に HTTP リクエストを含める → 1 RTT 削減
 
# ============================================
# TIME_WAIT 対策
# ============================================
net.ipv4.tcp_tw_reuse = 1
# TIME_WAIT 状態のソケットを新しい接続で再利用
# 注意: NAT 環境では慎重に使用(パケット混乱の可能性)
 
net.ipv4.ip_local_port_range = 1024 65535
# エフェメラルポートの範囲を拡大(デフォルト 32768-60999)
# TIME_WAIT 蓄積によるポート枯渇を緩和
 
# ============================================
# Keep-Alive 設定
# ============================================
net.ipv4.tcp_keepalive_time = 600      # 600 秒(10 分)後に Keep-Alive 開始
net.ipv4.tcp_keepalive_intvl = 60      # 60 秒間隔で Keep-Alive プローブ送信
net.ipv4.tcp_keepalive_probes = 3      # 3 回失敗で接続切断
 
# ============================================
# 接続数の上限
# ============================================
net.core.somaxconn = 65535             # listen() のバックログキューサイズ
net.core.netdev_max_backlog = 5000     # ネットワークデバイスのバックログ
 
# ============================================
# その他の最適化
# ============================================
net.ipv4.tcp_slow_start_after_idle = 0
# アイドル後のスロースタートを無効化(Keep-Alive 接続の高速化)
 
net.ipv4.tcp_mtu_probing = 1
# Path MTU Discovery の有効化(最適な MTU を自動検出)

設定の適用:

# 設定を即座に反映
$ sudo sysctl -p
 
# 設定の確認
$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = bbr
 
# 現在の TCP 統計
$ ss -s
Total: 342
TCP:   120 (estab 45, closed 20, orphaned 3, timewait 15)

注意事項:

  • 本番適用前に必ずステージング環境でテスト
  • tcp_tw_reuse は NAT 環境では慎重に(タイムスタンプの問題)
  • tcp_tw_recycle絶対に使わない(Linux 4.12 で削除済み)
  • BBR はカーネル 4.9 以降で使用可能

効果の計測:

# BBR 有効化前後での帯域幅計測
$ iperf3 -c target.example.com -t 30
 
# 結果例:
# CUBIC:  850 Mbps  (従来の輻輳制御)
# BBR:   1,100 Mbps  (30% 向上)

Q3: HTTP/3 によるパフォーマンス改善はどの程度か?

A: HTTP/3(QUIC)は特定の環境で大きな改善をもたらすが、すべてのケースで有効というわけではない。

HTTP/3 の主な改善点:

改善項目 HTTP/2 の課題 HTTP/3 の解決策 改善効果
Head-of-Line Blocking TCP レベルでパケットロスがあると全ストリームが停止 QUIC は UDP ベースで独立ストリーム パケットロス時 +30-50% 高速
接続確立 TCP (1.5 RTT) + TLS (1-2 RTT) = 2.5-3.5 RTT QUIC で統合 (0-1 RTT) 初回接続 -40%、再接続 -70% 高速化
接続マイグレーション IP 変更で接続切断(Wi-Fi ⇔ モバイル切替時) Connection ID で継続 モバイルユーザーの体験向上
輻輳制御 OS カーネル依存 QUIC 独自の輻輳制御 最適化の柔軟性向上

実測データ(Google の事例):

環境別の改善率:
環境レイテンシページロード時間
有線・低遅延(< 10ms)+5%+2-3%
Wi-Fi・中遅延(10-50ms)+10-15%+8-12%
モバイル・高遅延(50-200ms)+20-30%+15-25%
高パケットロス(1-5%)+40-60%+30-50%
結論: モバイル・高遅延・パケットロス環境で効果大

HTTP/3 の有効化(Nginx の例):

# Nginx 1.25.0 以降で HTTP/3 サポート
server {
    listen 443 ssl;
    listen 443 quic reuseport;  # HTTP/3 (QUIC) のリスニング
 
    ssl_certificate     /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;
 
    # HTTP/3 を Alt-Svc ヘッダーでアドバタイズ
    add_header Alt-Svc 'h3=":443"; ma=86400';
 
    # QUIC 用のパラメータ
    quic_retry on;
    ssl_early_data on;
 
    location / {
        # 通常の設定
    }
}

HTTP/3 の有効化確認:

# curl で HTTP/3 接続テスト(curl 7.66 以降)
$ curl --http3 -I https://example.com
 
# レスポンスヘッダーに Alt-Svc があるか確認
Alt-Svc: h3=":443"; ma=86400
 
# Chrome DevTools の Protocol 列で "h3" を確認
# または chrome://net-internals/#http3 で QUIC セッション確認

HTTP/3 を導入すべきケース:

  • ✅ モバイルユーザーが多い
  • ✅ グローバル展開(高遅延環境のユーザーがいる)
  • ✅ リアルタイム性が重要(動画ストリーミング、ゲーム)
  • ✅ パケットロスが発生しやすい環境

HTTP/3 が不要なケース:

  • ❌ イントラネット・ローカルネットワークのみ
  • ❌ サーバー間通信(API 間通信等)
  • ❌ レガシーシステム(対応コストが高い)

結論: HTTP/3 はモバイル・高遅延環境で 15-50% の改善が見込める。CDN(Cloudflare, CloudFront)経由なら設定のみで有効化可能。

Q4: Brotli圧縮とGzip圧縮はどう使い分けるべきか?

A: Brotliを優先し、Gzipをフォールバックとして設定するのが最適である。

圧縮率の比較(実測データ):

コンテンツ種別 元サイズ Gzip (level 6) Brotli (level 6) Brotli (level 11)
JavaScript (React) 500 KB 145 KB (71%) 125 KB (75%) 110 KB (78%)
CSS (Tailwind) 300 KB 45 KB (85%) 38 KB (87%) 32 KB (89%)
HTML 100 KB 25 KB (75%) 20 KB (80%) 17 KB (83%)
JSON (API) 200 KB 30 KB (85%) 25 KB (88%) 22 KB (89%)

実務での設定:

# Nginx での Brotli + Gzip 併用設定
# Brotli(モジュール別途インストール必要)
brotli on;
brotli_comp_level 6;          # 動的圧縮は 4-6 推奨(速度と圧縮率のバランス)
brotli_static on;             # 事前圧縮ファイル(.br)を優先配信
brotli_types text/plain text/css application/javascript application/json
             image/svg+xml application/xml;
 
# Gzip(Brotli非対応クライアント用フォールバック)
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/javascript application/json;

ビルド時の事前圧縮(推奨):

// vite.config.js
import viteCompression from 'vite-plugin-compression';
 
export default {
  plugins: [
    viteCompression({ algorithm: 'brotliCompress', ext: '.br', threshold: 1024 }),
    viteCompression({ algorithm: 'gzip', ext: '.gz', threshold: 1024 }),
  ]
};
// → dist/assets/app.a1b2c3.js     (元ファイル)
// → dist/assets/app.a1b2c3.js.br  (Brotli 事前圧縮)
// → dist/assets/app.a1b2c3.js.gz  (Gzip 事前圧縮)

結論: 静的アセットはビルド時にBrotli level 11で事前圧縮し、動的コンテンツはBrotli level 4-6でリアルタイム圧縮。CDN利用時はCDN側の自動圧縮に任せるのも有効。

Q5: リソースヒント(preconnect, prefetch, preload)の最適な使い分けは?

A: リソースヒントは過剰に使用すると帯域を圧迫するため、リソース種別に応じて厳選する。

使い分けの判断基準:

リソースヒントの選択フロー:

  現在のページで使う?
  ├── Yes → パーサーが早期発見する?
  │         ├── Yes → ヒント不要(通常のロード)
  │         └── No  → <link rel="preload">
  │                    例: CSS内のフォント、JS動的import
  │
  └── No → 次のページで使う?
           ├── Yes → <link rel="prefetch">
           │          例: 次ページのJS/CSS/データ
           └── 不明 → 使わない

具体的な設定例:

<head>
  <!-- preconnect: サードパーティへの早期接続(1ページ2-4個まで) -->
  <link rel="preconnect" href="https://api.example.com">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://cdn.example.com" crossorigin>
 
  <!-- dns-prefetch: preconnectの軽量版(多数のドメインがある場合) -->
  <link rel="dns-prefetch" href="https://analytics.example.com">
 
  <!-- preload: 現在ページの重要リソース(1ページ3-5個まで) -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/api/critical-data" as="fetch" crossorigin>
  <link rel="preload" as="image" href="/images/hero.webp" fetchpriority="high">
 
  <!-- prefetch: 次ページのリソース(アイドル時に取得) -->
  <link rel="prefetch" href="/next-page/bundle.js">
</head>

注意点:

  • preload は必ず 3 秒以内に使用されないと Chrome が警告を出す
  • prefetch はあくまで「ヒント」で、ブラウザが無視する場合がある
  • モバイルではデータ節約モード時に prefetch が無視される
  • preconnect が多すぎると逆にパフォーマンスが低下する(CPU/メモリ消費)

まとめ

概念 ポイント
レイテンシ CDN + preconnect + HTTP/2以上 + エッジコンピューティング
帯域 Brotli圧縮 + WebP/AVIF + コード分割 + Tree Shaking
キャッシュ Cache-Control + ETag + SWR + Service Worker
API フィールド選択 + Cursor-based pagination + バッチ + 接続プーリング
計測 Core Web Vitals + Lighthouse + RUM + CI/CD自動チェック
レジリエンス リトライ + サーキットブレーカー + ネットワーク状態検出
エッジ Edge Functions + KV Store + エッジDB

次に読むべきガイド

ネットワークパフォーマンス最適化の手法を習得したら、次は以下のトピックに進むことを推奨する。

  • ブラウザとWebプラットフォーム: Service Worker、キャッシュ API、Web Workers を活用したフロントエンド最適化を深掘りする
  • Webアプリケーション開発: React/Next.js でのコード分割、SSR/ISR、画像最適化など、フレームワーク固有の最適化手法を学ぶ
  • インフラストラクチャ: サーバーサイドのパフォーマンスチューニング(DB 最適化、キャッシュ戦略、水平スケール)を習得する

参考文献

  1. web.dev. "Web Performance." Google, 2024.
  2. Grigorik, I. "High Performance Browser Networking." O'Reilly, 2013.
  3. RFC 7932. "Brotli Compressed Data Format." IETF, 2016.
  4. RFC 9000. "QUIC: A UDP-Based Multiplexed and Secure Transport." IETF, 2021.
  5. RFC 9114. "HTTP/3." IETF, 2022.
  6. web.dev. "Core Web Vitals." Google, 2024.
  7. Cloudflare. "Workers Documentation." developers.cloudflare.com, 2024.
  8. Vercel. "Edge Functions." vercel.com/docs, 2024.
  9. workboxjs.org. "Workbox Documentation." Google, 2024.
  10. TanStack. "TanStack Query Documentation." tanstack.com, 2024.