この記事の要点
• Core Web Vitals(LCP・INP・CLS)がフロントエンドの重要指標
• N+1問題の解決・コネクションプール・レスポンス圧縮がバックエンド最適化の鍵
• マルチレイヤーキャッシュと適切なインデックス設計でデータベースを高速化
パフォーマンス指標
Core Web Vitals
| 指標 | 説明 | 目標値 |
|---|---|---|
| LCP | 最大コンテンツの描画時間 | < 2.5秒 |
| INP | 次の描画までのインタラクション遅延 | < 200ms |
| CLS | レイアウトのずれ | < 0.1 |
バックエンド指標
| 指標 | 説明 | 目標値 |
|---|---|---|
| TTFB | 最初のバイトまでの時間 | < 200ms |
| レスポンスタイム | API応答時間 | p95 < 500ms |
| スループット | 秒間リクエスト数 | システム依存 |
フロントエンド最適化
画像最適化
// Next.js Image最適化
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // LCP画像は優先読み込み
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
// サイズ別画像(srcset)
<picture>
<source
srcSet="/image-small.webp 480w, /image-medium.webp 768w, /image-large.webp 1200w"
type="image/webp"
/>
<img src="/image-fallback.jpg" alt="..." loading="lazy" />
</picture>
コード分割
// 動的インポート
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false, // クライアントのみ
});
// React.lazy
const LazyComponent = React.lazy(() => import('./LazyComponent'));
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
バンドル最適化
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
charts: ['recharts', 'd3'],
},
},
},
},
});
レンダリング最適化
// メモ化
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* 重い計算 */}</div>;
});
// useMemo
const sortedData = useMemo(() => {
return data.sort((a, b) => a.price - b.price);
}, [data]);
// 仮想スクロール
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={400}
itemCount={10000}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
バックエンド最適化
ポイント: フロントエンド最適化は「画像の最適化」「コード分割」「レンダリング最適化」の3つが主軸です。最も効果が大きいのは画像最適化です。
N+1問題の解決
// 悪い例(N+1)
const posts = await prisma.post.findMany();
for (const post of posts) {
post.author = await prisma.user.findUnique({
where: { id: post.authorId },
});
}
// 良い例(Eager Loading)
const posts = await prisma.post.findMany({
include: { author: true },
});
// DataLoaderパターン
const userLoader = new DataLoader(async (ids: string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: ids } },
});
return ids.map(id => users.find(u => u.id === id));
});
コネクションプール
// Prismaの設定
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
// 接続プールサイズ(URL内で設定)
// postgresql://user:pass@host/db?connection_limit=10&pool_timeout=20
レスポンス圧縮
import compression from 'compression';
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
level: 6, // 圧縮レベル(1-9)
}));
データベース最適化
注意: N+1問題はパフォーマンス劣化の最も一般的な原因です。ORM使用時は発行されるSQLを必ず確認してください。
インデックス戦略
-- 複合インデックス(頻繁なクエリパターンに基づく)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- 部分インデックス(特定条件のみ)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';
-- カバリングインデックス
CREATE INDEX idx_products_search ON products(name, price, category_id);
-- インデックス使用状況の確認
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
クエリ最適化
-- 必要なカラムのみ取得
SELECT id, name, email FROM users WHERE id = 1;
-- NG: SELECT * FROM users WHERE id = 1;
-- ページネーション(Cursor)
SELECT * FROM posts
WHERE id > :last_id
ORDER BY id
LIMIT 20;
-- バッチ処理
UPDATE products SET price = price * 1.1
WHERE category_id = 5
LIMIT 1000; -- 小分けに実行
実践メモ: EXPLAIN ANALYZEでクエリの実行計画を確認し、インデックスが効いているか定期的にチェックしましょう。
キャッシュ戦略
// マルチレイヤーキャッシュ
async function getProduct(id: string): Promise<Product> {
// L1: メモリキャッシュ
const memCached = memoryCache.get(`product:${id}`);
if (memCached) return memCached;
// L2: Redisキャッシュ
const redisCached = await redis.get(`product:${id}`);
if (redisCached) {
memoryCache.set(`product:${id}`, JSON.parse(redisCached), 60);
return JSON.parse(redisCached);
}
// L3: データベース
const product = await db.product.findUnique({ where: { id } });
// キャッシュに保存
await redis.setex(`product:${id}`, 3600, JSON.stringify(product));
memoryCache.set(`product:${id}`, product, 60);
return product;
}
計測とモニタリング
// Server Timing API
app.use((req, res, next) => {
const start = process.hrtime();
res.on('finish', () => {
const [seconds, nanoseconds] = process.hrtime(start);
const duration = seconds * 1000 + nanoseconds / 1000000;
res.setHeader('Server-Timing', `total;dur=${duration}`);
});
next();
});
関連記事
- キャッシュ戦略 - キャッシュの詳細
- CDN - コンテンツ配信
- データベースインデックス最適化 - DB高速化
- 監視・可観測性 - パフォーマンス監視
まとめ
パフォーマンス最適化は、フロントエンド(画像、コード分割)、バックエンド(N+1解決、圧縮)、データベース(インデックス、クエリ)の3層で取り組みます。計測→分析→改善のサイクルを回しましょう。
参考リソース
- web.dev - Web Vitals
- web.dev - Learn Performance
- MDN - Performance API
- Google - PageSpeed Insights
- Chrome DevTools - Lighthouse