パフォーマンス指標
Core Web Vitals
| 指標 | 説明 | 目標値 |
|---|
| LCP | 最大コンテンツの描画時間 | < 2.5秒 |
| INP | 次の描画までのインタラクション遅延 | < 200ms |
| CLS | レイアウトのずれ | < 0.1 |
バックエンド指標
| 指標 | 説明 | 目標値 |
|---|
| TTFB | 最初のバイトまでの時間 | < 200ms |
| レスポンスタイム | API応答時間 | p95 < 500ms |
| スループット | 秒間リクエスト数 | システム依存 |
フロントエンド最適化
画像最適化
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
<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,
});
const LazyComponent = React.lazy(() => import('./LazyComponent'));
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
バンドル最適化
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
charts: ['recharts', 'd3'],
},
},
},
},
});
レンダリング最適化
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{}</div>;
});
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>
バックエンド最適化
N+1問題の解決
const posts = await prisma.post.findMany();
for (const post of posts) {
post.author = await prisma.user.findUnique({
where: { id: post.authorId },
});
}
const posts = await prisma.post.findMany({
include: { author: true },
});
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));
});
コネクションプール
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
レスポンス圧縮
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,
}));
データベース最適化
インデックス戦略
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;
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;
キャッシュ戦略
async function getProduct(id: string): Promise<Product> {
const memCached = memoryCache.get(`product:${id}`);
if (memCached) return memCached;
const redisCached = await redis.get(`product:${id}`);
if (redisCached) {
memoryCache.set(`product:${id}`, JSON.parse(redisCached), 60);
return JSON.parse(redisCached);
}
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;
}
計測とモニタリング
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層で取り組みます。計測→分析→改善のサイクルを回しましょう。
← 一覧に戻る