パフォーマンス最適化 - Webアプリケーション高速化

20分 で読める | 2025.01.10

公式ドキュメント

パフォーマンス指標

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:YOUR_DB_PASSWORD@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();
});

関連記事

まとめ

パフォーマンス最適化は、フロントエンド(画像、コード分割)、バックエンド(N+1解決、圧縮)、データベース(インデックス、クエリ)の3層で取り組みます。計測→分析→改善のサイクルを回しましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR