23
✨ 飛躍編 Chapter 23

速さの追求

パフォーマンス最適化でユーザー体験を磨く

約55分
Performance · intro Optimization · intro Caching · intro
目次(29セクション)
🎬 Story — Introduction

お昼の12時。ShimaLinkのアクセスがピークを迎える時間帯。Mikaのスマホが鳴り続けていた。


Mika: 「また…『ページが開かない』って3件目のクレームが来た。お昼時に毎回こうなる。」

あなた: 「サーバーは落ちてないんだけど…なんか遅いんだよな。トップページの表示に8秒かかってる。」

あなた: 「8秒!? さすがにそれはユーザー離れるよね。」

あなた: 「でもどこがボトルネックなのかわからない。コード全体を見直すの?」


Yukiが自分のノートPCを開いて、ブラウザの開発者ツールを見せた。

Yuki: 「当てずっぽうで最適化するのは最悪のアプローチ。まず計測。どこが遅いかを数値で把握して、そこだけを直す。」

あなた: 「どうやって計測するんですか?」

Yuki: 「Googleが提唱するCore Web Vitalsって知ってる?ページの速さを測る3つの指標があるんだ。」

Yukiがホワイトボードに書いた。

LCP (Largest Contentful Paint) — 表示速度
FID (First Input Delay)        — 応答速度
CLS (Cumulative Layout Shift)  — 視覚的安定性

あなた: 「略語多いな…」

Yuki: 「簡単に言うと、“見えるまで何秒?”、“触れるまで何秒?”、“画面がガタガタしない?“の3つ。これを改善すれば、ユーザーは快適に感じる。」

Mika: 「お店のオーナーさんからも、もっとサクサク動いてほしいって要望は多いんだよね。」

Yuki: 「よし、ShimaLinkの速度を”体感できるレベル”まで改善しよう。フロントエンド、バックエンド、データベース——全方位から攻めるよ。」


ShimaLinkのパフォーマンスチューニングが始まる。目標は「どのページも2秒以内に表示」。

パフォーマンス指標 — Core Web Vitals

パフォーマンス改善の第一歩は「計測」。推測ではなく、数値に基づいて最適化する。

Yuki: 「“推測するな、計測せよ”。これがパフォーマンスチューニングの鉄則だよ。」

Core Web Vitals とは?

Googleが定義した、ユーザー体験を測る3つの主要指標です。

指標意味良い要改善悪い
LCP最大コンテンツの表示時間≤ 2.5s≤ 4.0s> 4.0s
FID最初の入力の遅延時間≤ 100ms≤ 300ms> 300ms
CLSレイアウトのずれ度合い≤ 0.1≤ 0.25> 0.25

LCP(Largest Contentful Paint)

ページの最大要素(メイン画像やテキストブロック)が表示されるまでの時間。

ユーザーの感覚:
< 1秒   → 「速い!」
1-2.5秒 → 「普通」
> 4秒   → 「遅い...閉じよう」

FID(First Input Delay) / INP

ユーザーが最初にクリックやタップした時、反応するまでの時間。

注意: FIDは2024年3月にINP(Interaction to Next Paint)に置き換えられました。INPはすべてのインタラクションの応答性を測定します。

CLS(Cumulative Layout Shift)

ページの読み込み中に要素が「ガタガタ」と動く度合い。

悪い例: 広告が後から読み込まれて、記事のテキストが下にズレる
良い例: 画像のサイズが予約されていて、レイアウトが安定している

計測ツール

Lighthouse(ブラウザ内蔵)

1. Chrome DevToolsを開く(F12)
2. 「Lighthouse」タブを選択
3. 「Analyze page load」をクリック
4. スコアと改善提案が表示される

PageSpeed Insights(Web版)

https://pagespeed.web.dev/
→ URLを入力するだけで、実際のユーザーデータも含めたレポートが見られる

コマンドラインで実行

# Lighthouse CLIのインストール
npm install -g lighthouse

# レポートを生成
lighthouse https://shimalink.example.com --output html --output-path report.html

# モバイルとデスクトップ両方
lighthouse https://shimalink.example.com --preset=desktop

Chrome DevToolsで計測する

Performanceタブ

1. DevToolsを開く(F12)
2. Performanceタブを選択
3. ⚫ 録画ボタンを押す
4. ページをリロード
5. 録画を止めて、タイムラインを確認

Networkタブ

1. Networkタブを開く
2. ページをリロード
3. 「Waterfall」で各リクエストの時間を確認
4. 大きなファイルや遅いリクエストを特定
確認項目見るべきこと
リクエスト数多すぎないか?(目安: 50以下)
転送量大きすぎないか?(目安: 1MB以下)
TTFBサーバーの応答が遅くないか?
画像サイズ最適化されているか?

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

チームで「これ以上遅くしない」というルールを決めましょう。

{
  "budgets": [
    {
      "resourceType": "total",
      "budget": 500
    },
    {
      "resourceType": "script",
      "budget": 200
    },
    {
      "resourceType": "image",
      "budget": 200
    },
    {
      "metric": "largest-contentful-paint",
      "budget": 2500
    }
  ]
}
指標バジェット例
ページサイズ合計≤ 500KB
JavaScript≤ 200KB
画像≤ 200KB
LCP≤ 2.5s
TTI≤ 3.0s

ポイント

  • パフォーマンス改善は「計測 → 分析 → 改善 → 再計測」のサイクル
  • Core Web Vitals(LCP, FID/INP, CLS)がユーザー体験の指標
  • Lighthouse, DevTools, PageSpeed Insightsで計測
  • パフォーマンスバジェットでチームのルールを明確にする
  • 推測ではなく、データに基づいて最適化する

フロントエンド最適化 — 表示速度を極める

計測で問題箇所が判明した。まずはフロントエンドから最適化しよう。

Yuki: 「フロントエンドの最適化は、ユーザーが最も直接体感する改善。1秒速くなるだけで離脱率が大きく減るよ。」

画像の最適化

画像はページサイズの大部分を占めることが多い。

適切なフォーマットの選択

フォーマット用途特徴
WebP写真全般JPEGより30%小さい
AVIF写真(最新)WebPよりさらに小さい
SVGアイコン、ロゴベクター画像、拡大しても劣化しない
PNG透過が必要な画像ファイルサイズが大きくなりがち

レスポンシブ画像

<!-- 画面サイズに応じて適切なサイズを読み込む -->
<img
  src="shop-800w.webp"
  srcset="
    shop-400w.webp 400w,
    shop-800w.webp 800w,
    shop-1200w.webp 1200w
  "
  sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px"
  alt="海風テラスの外観"
  width="800"
  height="600"
  loading="lazy"
/>

遅延読み込み(Lazy Loading)

<!-- ファーストビュー以外の画像はlazyで読み込む -->
<img src="photo.webp" loading="lazy" alt="店舗写真" />

<!-- ファーストビューの画像はeagerで優先読み込み -->
<img src="hero.webp" loading="eager" fetchpriority="high" alt="メインビジュアル" />

CSSの最適化

クリティカルCSSのインライン化

<head>
  <!-- ファーストビューに必要なCSSだけインラインで -->
  <style>
    .hero { background: #0077b6; color: white; padding: 2rem; }
    .nav { display: flex; gap: 1rem; }
  </style>

  <!-- 残りのCSSは非同期で読み込む -->
  <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>

不要なCSSの削除

# PurgeCSSで未使用CSSを除去
npx purgecss --css styles.css --content index.html --output cleaned.css

JavaScriptの最適化

コード分割(Code Splitting)

// 悪い例: すべてを一括インポート
import { HeavyChart } from "./HeavyChart";

// 良い例: 必要な時だけ読み込む(動的インポート)
const HeavyChart = React.lazy(() => import("./HeavyChart"));

function Dashboard() {
  return (
    <Suspense fallback={<div>読み込み中...</div>}>
      <HeavyChart />
    </Suspense>
  );
}

バンドルサイズの分析

# webpack-bundle-analyzerでバンドルの中身を可視化
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json

# Viteの場合
npx vite-bundle-visualizer

Tree Shaking

// 悪い例: ライブラリ全体をインポート
import _ from "lodash";          // 72KB
_.debounce(handler, 300);

// 良い例: 必要な関数だけインポート
import debounce from "lodash/debounce";  // 4KB
debounce(handler, 300);

CLSの改善

画像のサイズを予約

<!-- 悪い例: サイズ未指定(レイアウトシフトが起きる) -->
<img src="photo.webp" alt="写真" />

<!-- 良い例: width/heightを指定 -->
<img src="photo.webp" alt="写真" width="800" height="600" />

<!-- CSSでアスペクト比を保つ -->
<style>
  .shop-image {
    aspect-ratio: 4 / 3;
    width: 100%;
    object-fit: cover;
  }
</style>

Webフォントの最適化

<!-- フォントの先読み -->
<link rel="preload" href="/fonts/NotoSansJP.woff2" as="font" type="font/woff2" crossorigin>

<!-- フォント読み込み中のちらつきを防ぐ -->
<style>
  @font-face {
    font-family: 'Noto Sans JP';
    src: url('/fonts/NotoSansJP.woff2') format('woff2');
    font-display: swap; /* フォントが読み込まれるまでシステムフォントを表示 */
  }
</style>

最適化チェックリスト

□ 画像をWebP/AVIFフォーマットに変換
□ 画像にwidth/heightを指定(CLS対策)
□ ファーストビュー外の画像にloading="lazy"
□ JavaScriptをコード分割(動的インポート)
□ バンドルサイズを分析して不要なライブラリを削除
□ クリティカルCSSをインライン化
□ Webフォントにfont-display: swapを設定
□ リソースの先読み(preload/prefetch)

ポイント

  • 画像最適化(WebP、lazy loading、srcset)が最も効果が大きい
  • JavaScriptのコード分割とTree Shakingでバンドルサイズを削減
  • CLS対策には画像のサイズ予約とfont-display: swap
  • クリティカルCSSのインライン化でファーストビューを高速化
  • 最適化前後で必ず計測して効果を確認する

バックエンド最適化 & キャッシュ戦略

フロントエンドを改善しても、サーバーの応答が遅ければ意味がない。

Yuki: 「最速のリクエストは、そもそも発生しないリクエスト。キャッシュはパフォーマンスの最強の武器だよ。」

キャッシュの基本概念

キャッシュとは、一度取得したデータを保存しておき、次回はそのデータを返すことです。

1回目: クライアント → サーバー → DB (200ms)
2回目: クライアント → キャッシュ (5ms) ← 40倍速い!

キャッシュの層

場所TTL目安
ブラウザキャッシュクライアント分〜日静的ファイル
CDNキャッシュエッジサーバー分〜時間HTML、画像
アプリキャッシュサーバーメモリ秒〜分APIレスポンス
DBキャッシュRedis等秒〜分クエリ結果

HTTPキャッシュヘッダー

// Express.js での設定例

// 静的ファイル: 1年キャッシュ(ファイル名にハッシュ含む場合)
app.use("/static", express.static("public", {
  maxAge: "1y",
  immutable: true
}));

// API: 5分キャッシュ
app.get("/api/shops", (req, res) => {
  res.set("Cache-Control", "public, max-age=300");
  res.json(shops);
});

// ユーザー固有データ: キャッシュしない
app.get("/api/profile", (req, res) => {
  res.set("Cache-Control", "private, no-cache");
  res.json(profile);
});

Cache-Controlの主な値

意味
publicCDN・共有キャッシュOK
privateブラウザのみキャッシュOK
max-age=300300秒間キャッシュ
no-cache毎回サーバーに確認
no-storeキャッシュ禁止
immutable変更されない(ハッシュ付きファイル向け)

Redisによるアプリケーションキャッシュ

# Redisのインストール(Docker)
docker run -d --name redis -p 6379:6379 redis
import Redis from "ioredis";

const redis = new Redis();

// キャッシュ付きデータ取得
async function getShopsWithCache() {
  const cacheKey = "shops:all";

  // 1. キャッシュをチェック
  const cached = await redis.get(cacheKey);
  if (cached) {
    console.log("Cache HIT");
    return JSON.parse(cached);
  }

  // 2. キャッシュミス: DBから取得
  console.log("Cache MISS");
  const shops = await db.query("SELECT * FROM shops WHERE active = true");

  // 3. キャッシュに保存(5分間)
  await redis.setex(cacheKey, 300, JSON.stringify(shops));

  return shops;
}

// キャッシュの無効化(データ更新時)
async function updateShop(id: string, data: any) {
  await db.query("UPDATE shops SET ... WHERE id = $1", [id]);

  // 関連するキャッシュを削除
  await redis.del("shops:all");
  await redis.del(`shops:${id}`);
}

レスポンスの圧縮

import compression from "compression";

// gzip/brotli圧縮を有効化
app.use(compression());

// 効果: JSONレスポンスが70-80%小さくなる
// 100KB → 20KB

N+1問題の解決

データベースへの無駄なクエリを減らす。

// 悪い例: N+1問題(店舗ごとにクエリが発生)
const shops = await db.query("SELECT * FROM shops");
for (const shop of shops) {
  // 各店舗のレビューを個別に取得(100店舗 → 101クエリ!)
  shop.reviews = await db.query(
    "SELECT * FROM reviews WHERE shop_id = $1", [shop.id]
  );
}

// 良い例: JOINまたはIN句で一括取得(2クエリで完了)
const shops = await db.query("SELECT * FROM shops");
const shopIds = shops.map(s => s.id);
const reviews = await db.query(
  "SELECT * FROM reviews WHERE shop_id = ANY($1)", [shopIds]
);

// メモリ上で結合
const reviewsByShop = reviews.reduce((acc, r) => {
  (acc[r.shop_id] = acc[r.shop_id] || []).push(r);
  return acc;
}, {});

shops.forEach(shop => {
  shop.reviews = reviewsByShop[shop.id] || [];
});

API レスポンスの最適化

// 悪い例: 不要なデータまで返す
app.get("/api/shops", async (req, res) => {
  const shops = await db.query("SELECT * FROM shops"); // 全カラム
  res.json(shops);
});

// 良い例: 必要なフィールドだけ返す
app.get("/api/shops", async (req, res) => {
  const shops = await db.query(
    "SELECT id, name, category, rating, thumbnail_url FROM shops WHERE active = true"
  );
  res.json(shops);
});

// ページネーション
app.get("/api/shops", async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const offset = (page - 1) * limit;

  const shops = await db.query(
    "SELECT id, name, category, rating FROM shops LIMIT $1 OFFSET $2",
    [limit, offset]
  );

  res.json({ data: shops, page, limit });
});

ポイント

  • キャッシュは多層で適用: ブラウザ → CDN → アプリ → DB
  • Cache-Controlヘッダーでブラウザキャッシュを制御
  • Redisでアプリケーションレベルのキャッシュを実装
  • レスポンス圧縮(gzip/brotli)で転送量を80%削減
  • N+1問題を解決してDBクエリ数を削減
  • 必要なデータだけ返し、ページネーションを実装する

データベース最適化 — クエリを高速化する

バックエンドのボトルネックの多くは、データベースにある。

Yuki: 「インデックスを1つ追加するだけで、クエリが100倍速くなることもある。データベースの最適化は費用対効果が最高だよ。」

クエリのプロファイリング

まずは遅いクエリを特定しよう。

EXPLAIN ANALYZE

-- クエリの実行計画を確認
EXPLAIN ANALYZE
SELECT * FROM shops WHERE category = 'カフェ' AND rating >= 4.0;

-- 出力例:
-- Seq Scan on shops  (cost=0.00..2.50 rows=5 width=128)
--   Filter: ((category = 'カフェ') AND (rating >= 4.0))
--   Rows Removed by Filter: 95
--   Planning Time: 0.08 ms
--   Execution Time: 0.25 ms
注目すべき点意味
Seq Scan全行スキャン(遅い可能性)
Index Scanインデックス使用(速い)
Rows Removedフィルタで除外された行数
Execution Time実際の実行時間

インデックス — 最も効果的な最適化

インデックスは「本の索引」のようなもの。全ページめくらなくても目的の内容にすぐ辿り着ける。

インデックスの作成

-- 基本的なインデックス
CREATE INDEX idx_shops_category ON shops(category);

-- 複合インデックス(よく一緒に検索されるカラム)
CREATE INDEX idx_shops_category_rating ON shops(category, rating);

-- 部分インデックス(条件付き)
CREATE INDEX idx_active_shops ON shops(category)
WHERE active = true;

Before / After

-- Before: インデックスなし
EXPLAIN ANALYZE SELECT * FROM shops WHERE category = 'カフェ';
-- Seq Scan: 15.2ms(全100,000行をスキャン)

-- After: インデックスあり
CREATE INDEX idx_shops_category ON shops(category);
EXPLAIN ANALYZE SELECT * FROM shops WHERE category = 'カフェ';
-- Index Scan: 0.15ms(100倍速い!)

インデックスの注意点

すべきこと避けるべきこと
WHERE句で頻繁に使うカラム更新が頻繁なカラムへの過剰なインデックス
JOINのキーカラム小さなテーブル(数百行以下)
ORDER BYで使うカラム選択性が低いカラム(true/falseなど)

クエリの書き方を改善

SELECTの最適化

-- 悪い例: 全カラム取得
SELECT * FROM shops;

-- 良い例: 必要なカラムだけ
SELECT id, name, category, rating FROM shops;

JOINの最適化

-- 悪い例: サブクエリの連発
SELECT s.*,
  (SELECT COUNT(*) FROM reviews WHERE shop_id = s.id) as review_count,
  (SELECT AVG(rating) FROM reviews WHERE shop_id = s.id) as avg_rating
FROM shops s;

-- 良い例: JOINで一括取得
SELECT
  s.id, s.name, s.category,
  COUNT(r.id) as review_count,
  AVG(r.rating) as avg_rating
FROM shops s
LEFT JOIN reviews r ON r.shop_id = s.id
GROUP BY s.id, s.name, s.category;

LIMITとOFFSETの最適化

-- 遅い例: 大きなオフセット(10万行スキップ)
SELECT * FROM shops ORDER BY created_at LIMIT 20 OFFSET 100000;

-- 速い例: カーソルベースのページネーション
SELECT * FROM shops
WHERE created_at < '2025-03-01T00:00:00Z'
ORDER BY created_at DESC
LIMIT 20;

コネクションプーリング

データベース接続は高コスト。接続プールで使い回す。

import { Pool } from "pg";

// コネクションプール設定
const pool = new Pool({
  host: process.env.DB_HOST,
  database: "shimalink",
  max: 20,              // 最大接続数
  idleTimeoutMillis: 30000,  // アイドルタイムアウト
  connectionTimeoutMillis: 2000  // 接続タイムアウト
});

// プールを使ったクエリ
async function getShops(category: string) {
  const client = await pool.connect();
  try {
    const result = await client.query(
      "SELECT id, name, rating FROM shops WHERE category = $1 ORDER BY rating DESC",
      [category]
    );
    return result.rows;
  } finally {
    client.release(); // 必ず接続をプールに返す
  }
}

スロークエリログ

-- PostgreSQLのスロークエリログを有効化
ALTER SYSTEM SET log_min_duration_statement = 100;  -- 100ms以上を記録
SELECT pg_reload_conf();

-- 確認
SHOW log_min_duration_statement;
# ログの確認
tail -f /var/log/postgresql/postgresql.log | grep duration

最適化の優先順位

1. [最優先] インデックスの追加        → 効果大、リスク小
2. [高]     クエリの書き直し          → 効果大、作業中
3. [中]     コネクションプール設定    → 安定性向上
4. [中]     SELECT *の排除           → 転送量削減
5. [低]     テーブル設計の見直し      → 効果大だが影響範囲大

ポイント

  • EXPLAIN ANALYZEで遅いクエリを特定する
  • インデックスは最も費用対効果の高い最適化手段
  • SELECT * を避け、必要なカラムだけ取得する
  • N+1問題をJOINやIN句で解決する
  • コネクションプーリングでDB接続を効率化する
  • スロークエリログで継続的にパフォーマンスを監視する
📖 Story — Conclusion

最適化から1週間後。お昼のピーク時間。

Lighthouse Score: 95/100
LCP: 1.2s (Good ✓)
FID: 45ms  (Good ✓)
CLS: 0.02  (Good ✓)

Page Load: 1.8s (target: <2s ✓)

あなた: 「8秒が1.8秒…4倍以上速くなった!しかもピーク時でも安定してる。」

あなた: 「キャッシュが効いてるね。2回目のアクセスは0.5秒で表示される。」

Mika: 「お店のオーナーさんから『最近サクサク動くね!』ってメッセージ来た!クレームはゼロ!」

Yuki: 「パフォーマンスの改善は、ユーザーが一番体感しやすい改善なんだ。機能を増やさなくても、速くするだけで満足度が上がる。」

あなた: 「一番効果があったのはデータベースのインデックスだったな。1つ追加しただけでAPIレスポンスが10倍速くなった。」

Yuki: 「“推測するな、計測せよ”。この原則を忘れなければ、パフォーマンスは必ず改善できる。ところでさ…最近、あなたが面白いこと言ってなかった?」

あなた: 「あ、そうだ!AIを使ってお店の紹介文を自動生成できたら、オーナーさんの負担が減ると思うんだけど…」

Mika: 「AIチャットボットでお客さんの質問に自動で答えられたら、私の対応時間もすごく減る!」

Yuki: 「時代の流れに乗るのは大事だね。次はAIとの共創を学ぼう。」


次のチャプター: Chapter 24: AIとの共創 — ShimaLinkにAI機能を導入。紹介文の自動生成、チャットボット、プロンプトエンジニアリングを学ぶ。

🧠 理解度チェック

Q1.Core Web VitalsのLCP(Largest Contentful Paint)の「良い」基準は?

💡 ShimaLinkのLCPが8秒だった時、Yukiが「2.5秒以下を目指そう」と言っていたね。

Q2.CLS(Cumulative Layout Shift)を改善するために最も効果的な対策は?

💡 店舗写真が後から読み込まれてレイアウトがガタガタしていた問題、width/heightで解決したよね。

Q3.Cache-Control: public, max-age=300 の意味は?

💡 ShimaLinkの店舗一覧APIに設定した、あのHTTPキャッシュヘッダーの設定だよ。

Q4.データベースのインデックスの説明として正しいのは?

💡 カテゴリ検索が15msから0.15msになった、あのインデックス追加のインパクトを思い出そう。

Q5.N+1問題とは何か?

💡 100店舗のレビューを取得するのに101回もDBクエリが走っていた、あの問題だね。

Q6.画像の最適化で最も推奨されるフォーマットは?

💡 ShimaLinkの店舗画像をJPEGからWebPに変換して、ページサイズが大幅に減った場面を覚えてる?

Q7.パフォーマンスチューニングの鉄則は?

💡 Yukiが最初に教えてくれた鉄則。Lighthouseで計測して、ボトルネックを特定してから改善したよね。

よくある質問

Lighthouseのスコアが毎回変わる

Lighthouseのスコアは**テスト環境や状況**によってブレます。 **安定した計測のコツ:** - シークレットモードで実行(拡張機能の影響を排除) - 3回以上測定して中央値を取る - 他のアプリやタブを閉じる - 同じネットワーク環境で測定する ```bash # CLIで複数回実行して比較 for i in 1 2 3; do lighthouse https://example.com --output json --quiet \ | jq '.categories.performance.score' done ``` **PageSpeed Insights** はフィールドデータ(実際のユーザーデータ)も表示するので、より信頼性が高いです。

WebPフォーマットに変換する方法は?

**コマンドラインで変換:** ```bash # cwebpコマンド(macOS) brew install webp cwebp input.jpg -o output.webp -q 80 # 一括変換 for f in *.jpg; do cwebp "$f" -o "${f%.jpg}.webp" -q 80 done ``` **ビルドツールで自動変換:** ```javascript // vite.config.ts import { imagetools } from 'vite-imagetools'; export default { plugins: [imagetools()] } ``` **HTMLで対応ブラウザ分岐:** ```html <picture> <source srcset="photo.avif" type="image/avif"> <source srcset="photo.webp" type="image/webp"> <img src="photo.jpg" alt="店舗写真"> </picture> ```

Redisのインストール方法がわからない

**Docker(推奨):** ```bash docker run -d --name redis -p 6379:6379 redis:alpine # 接続確認 docker exec -it redis redis-cli ping # → PONG ``` **Mac(Homebrew):** ```bash brew install redis brew services start redis redis-cli ping ``` **Node.jsから接続:** ```bash npm install ioredis ``` ```typescript import Redis from 'ioredis'; const redis = new Redis(); // localhost:6379 await redis.set('key', 'value'); const val = await redis.get('key'); ```

EXPLAIN ANALYZEの出力の読み方がわからない

**重要な項目:** | 項目 | 意味 | 注目ポイント | |------|------|-------------| | **Seq Scan** | 全行スキャン | テーブルが大きい場合は遅い | | **Index Scan** | インデックス使用 | 速い、理想的 | | **rows** | 推定行数 | 実際の行数と大きくずれていないか | | **Execution Time** | 実行時間 | 目標: 10ms以下 | ```sql EXPLAIN ANALYZE SELECT * FROM shops WHERE category = 'カフェ'; -- Seq Scan が表示されたら→インデックスを追加 CREATE INDEX idx_shops_category ON shops(category); -- 再度実行して Index Scan になることを確認 ```

バンドルサイズが大きすぎる

**原因の特定:** ```bash # Viteの場合 npx vite-bundle-visualizer # webpackの場合 npx webpack-bundle-analyzer dist/stats.json ``` **よくある原因と対策:** | 原因 | 対策 | |------|------| | lodash全体のインポート | `import debounce from 'lodash/debounce'` | | moment.jsの使用 | `date-fns` や `dayjs` に置き換え | | アイコンライブラリ全体 | 必要なアイコンだけインポート | | 未使用コード | Tree Shakingの確認 | **動的インポートで分割:** ```typescript const HeavyComponent = React.lazy(() => import('./Heavy')); ```

キャッシュを更新したのに古いデータが表示される

**キャッシュの無効化(Cache Busting):** 1. **ブラウザキャッシュ:** ファイル名にハッシュを付ける ```html <!-- ビルドツールが自動でハッシュを付与 --> <script src="app.a1b2c3d4.js"></script> ``` 2. **CDNキャッシュ:** パージ(削除)する ```bash # CloudflareのAPIでパージ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer TOKEN" \ -d '{"purge_everything":true}' ``` 3. **Redisキャッシュ:** キーを削除する ```typescript await redis.del('shops:all'); // または全削除 await redis.flushdb(); ``` **原則:** データ更新時に関連するキャッシュを必ず無効化する。

lazy loadingが効いてるか確認する方法は?

**DevToolsで確認:** 1. Networkタブを開く 2. ページを読み込む 3. `loading="lazy"` の画像が**すぐにリクエストされていないこと**を確認 4. スクロールして画像が表示される位置まで移動 5. その時点でリクエストが発生することを確認 ```html <!-- lazy loadingの確認 --> <img src="photo.webp" loading="lazy" alt="テスト" /> ``` **注意:** ファーストビューの画像には `loading="lazy"` を使わないこと。LCPが悪化します。 ```html <!-- ファーストビューの画像 --> <img src="hero.webp" loading="eager" fetchpriority="high" alt="メイン" /> ```

インデックスを追加したらINSERTが遅くなった

**インデックスにはトレードオフがあります:** | 操作 | インデックスあり | インデックスなし | |------|----------------|----------------| | SELECT | 速い | 遅い | | INSERT | やや遅い | 速い | | UPDATE | やや遅い | 速い | | DELETE | やや遅い | 速い | **対策:** - 本当に必要なインデックスだけ作る - 使われていないインデックスは削除する - 大量INSERTの前にインデックスを一時削除し、後で再作成 ```sql -- 使われていないインデックスの確認(PostgreSQL) SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE idx_scan = 0; ```

コネクションプールのmax値はいくつに設定すべき?

**一般的な目安:** ``` max = (CPUコア数 × 2) + ディスク数 ``` 例: 4コアCPU + SSD1台 → max = 9〜10 **実際の設定例:** ```typescript const pool = new Pool({ max: 20, // 小〜中規模アプリ idleTimeoutMillis: 30000, // 30秒でアイドル接続を解放 connectionTimeoutMillis: 5000 // 5秒で接続タイムアウト }); ``` **多すぎるとDB側で問題が起きます。** PostgreSQLのデフォルト最大接続数は100です。複数のアプリサーバーがある場合は分配が必要です。 ```sql SHOW max_connections; -- → 100 ```

Core Web VitalsとSEOの関係は?

**GoogleはCore Web VitalsをSEOのランキング要因に含めています。** | 指標 | SEOへの影響 | |------|------------| | LCP | ページ表示速度が遅いと順位低下 | | FID/INP | インタラクション応答が遅いと順位低下 | | CLS | レイアウトずれが大きいと順位低下 | **Google Search Consoleで確認:** 1. Search Console → 「ウェブに関する主な指標」 2. 「良好」「改善が必要」「不良」のURLが表示される **つまり:** パフォーマンス改善はUX向上だけでなく、検索順位の向上にもつながります。ShimaLinkの店舗ページが検索で上位に表示されるためにも重要です。