パフォーマンスの深い理解
パフォーマンスの深い理解
Section titled “パフォーマンスの深い理解”シニアエンジニアとして、Next.jsアプリケーションのパフォーマンスを最適化するには、表面的な最適化ではなく、ブラウザとNext.jsの動作を深く理解する必要があります。この章では、パフォーマンスの根本的な理解について解説します。
Core Web Vitalsの深い理解
Section titled “Core Web Vitalsの深い理解”LCP (Largest Contentful Paint)
Section titled “LCP (Largest Contentful Paint)”LCPとは何か:
LCPは、ページの主要コンテンツが表示されるまでの時間を測定します。
LCPに影響する要素:
// 問題のあるコード: LCPが遅いexport default function ProductPage({ params }: { params: { id: string } }) { return ( <div> {/* 問題: 画像がLCP要素だが最適化されていない */} <img src="/large-product-image.jpg" alt="Product" /> <h1>Product Name</h1> </div> );}
// 解決: Next.js Imageコンポーネントを使用import Image from 'next/image';
export default function ProductPage({ params }: { params: { id: string } }) { return ( <div> {/* 解決: 画像が最適化され、優先的にロードされる */} <Image src="/large-product-image.jpg" alt="Product" width={1200} height={800} priority // LCP要素を優先的にロード /> <h1>Product Name</h1> </div> );}
// LCPの最適化戦略:// 1. LCP要素を特定(通常は画像、動画、または大きなテキストブロック)// 2. LCP要素を優先的にロード(priorityプロパティ)// 3. 画像の最適化(next/imageを使用)// 4. サーバーサイドレンダリングでLCP要素を含めるFID (First Input Delay)
Section titled “FID (First Input Delay)”FIDとは何か:
FIDは、ユーザーが最初にインタラクション(クリック、タップ、キー入力)してから、ブラウザがそのインタラクションに応答するまでの時間を測定します。
FIDに影響する要素:
// 問題のあるコード: メインスレッドがブロックされる'use client';
export default function HeavyComponent() { // 問題: 重い処理がメインスレッドをブロック const processHeavyData = () => { const data = Array.from({ length: 1000000 }, (_, i) => i); return data.map(x => x * 2).filter(x => x > 1000); };
const [result, setResult] = useState(null);
useEffect(() => { // 問題: 同期的な重い処理がFIDを悪化させる const processed = processHeavyData(); setResult(processed); }, []);
return <div>{result?.length}</div>;}
// 解決1: Web Workerを使用// worker.tsself.onmessage = (e) => { const data = Array.from({ length: 1000000 }, (_, i) => i); const processed = data.map(x => x * 2).filter(x => x > 1000); self.postMessage(processed);};
// Component.tsx'use client';
export default function HeavyComponent() { const [result, setResult] = useState(null);
useEffect(() => { const worker = new Worker(new URL('./worker.ts', import.meta.url)); worker.onmessage = (e) => { setResult(e.data); }; worker.postMessage('start');
return () => worker.terminate(); }, []);
return <div>{result?.length}</div>;}
// 解決2: 時間を分割して処理(Time Slicing)'use client';
export default function HeavyComponent() { const [result, setResult] = useState<number[]>([]);
useEffect(() => { const processInChunks = async () => { const data = Array.from({ length: 1000000 }, (_, i) => i); const chunkSize = 10000;
for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize); const processed = chunk.map(x => x * 2).filter(x => x > 1000);
setResult(prev => [...prev, ...processed]);
// メインスレッドを解放 await new Promise(resolve => setTimeout(resolve, 0)); } };
processInChunks(); }, []);
return <div>{result.length}</div>;}CLS (Cumulative Layout Shift)
Section titled “CLS (Cumulative Layout Shift)”CLSとは何か:
CLSは、ページの読み込み中にレイアウトがどれだけシフトするかを測定します。
CLSに影響する要素:
// 問題のあるコード: レイアウトシフトが発生export default function ProductList() { const [products, setProducts] = useState([]);
useEffect(() => { fetchProducts().then(setProducts); }, []);
return ( <div> {products.map(product => ( // 問題: 画像のサイズが指定されていない <img src={product.image} alt={product.name} /> ))} </div> );}
// 解決: 画像のサイズを事前に指定import Image from 'next/image';
export default function ProductList() { const [products, setProducts] = useState([]);
useEffect(() => { fetchProducts().then(setProducts); }, []);
return ( <div> {products.map(product => ( // 解決: サイズを指定してレイアウトシフトを防止 <Image src={product.image} alt={product.name} width={300} height={300} /> ))} </div> );}
// 解決: スケルトンローディングでプレースホルダーを表示export default function ProductList() { const [products, setProducts] = useState([]); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { fetchProducts().then(data => { setProducts(data); setIsLoading(false); }); }, []);
if (isLoading) { // スケルトンでレイアウトを確保 return ( <div> {Array.from({ length: 10 }).map((_, i) => ( <div key={i} className="h-64 w-64 bg-gray-200 animate-pulse" /> ))} </div> ); }
return ( <div> {products.map(product => ( <Image src={product.image} alt={product.name} width={300} height={300} /> ))} </div> );}バンドルサイズの最適化
Section titled “バンドルサイズの最適化”コード分割の戦略
Section titled “コード分割の戦略”動的インポートの活用:
// 問題のあるコード: すべてのコードを1つのバンドルにimport HeavyComponent from '@/components/HeavyComponent';import ChartLibrary from '@/lib/chart-library';
export default function Page() { return ( <div> <HeavyComponent /> <ChartLibrary /> </div> );}
// 解決: 動的インポートでコード分割import dynamic from 'next/dynamic';
// 動的インポート(コード分割)const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), { loading: () => <p>Loading...</p>, ssr: false, // サーバーサイドレンダリングを無効化});
const ChartLibrary = dynamic(() => import('@/lib/chart-library'), { loading: () => <p>Loading chart...</p>,});
export default function Page() { return ( <div> <HeavyComponent /> <ChartLibrary /> </div> );}
// 条件付きロードconst AdminPanel = dynamic(() => import('@/components/AdminPanel'), { loading: () => <p>Loading admin panel...</p>,});
export default function Dashboard({ isAdmin }: { isAdmin: boolean }) { return ( <div> <UserDashboard /> {isAdmin && <AdminPanel />} // 必要な時だけロード </div> );}Tree Shakingの最適化:
// 問題のあるコード: ライブラリ全体をインポートimport _ from 'lodash';
export default function Component() { const result = _.uniq([1, 2, 2, 3]); // lodash全体がバンドルに含まれる return <div>{result}</div>;}
// 解決: 必要な関数だけをインポートimport uniq from 'lodash/uniq';
export default function Component() { const result = uniq([1, 2, 2, 3]); // uniqだけがバンドルに含まれる return <div>{result}</div>;}
// さらに良い: ネイティブの実装を使用export default function Component() { const result = [...new Set([1, 2, 2, 3])]; // ライブラリ不要 return <div>{result}</div>;}キャッシング戦略の深い理解
Section titled “キャッシング戦略の深い理解”Next.jsのキャッシングレイヤー
Section titled “Next.jsのキャッシングレイヤー”キャッシングの階層:
1. Request Memoization - 同一リクエスト内での重複リクエストを防ぐ - 自動的に適用される
2. Data Cache (fetch cache) - fetch()の結果をキャッシュ - revalidateオプションで制御
3. Full Route Cache - レンダリング結果をキャッシュ - 静的生成されたページ
4. Router Cache - クライアント側のルーターキャッシュ - ナビゲーション時の高速化実践的なキャッシング設定:
// 1. 常に最新のデータが必要export default async function Dashboard() { const data = await fetch('https://api.example.com/dashboard', { cache: 'no-store', // キャッシュしない }); return <Dashboard data={data} />;}
// 2. 定期的に更新(ISR)export default async function Products() { const data = await fetch('https://api.example.com/products', { next: { revalidate: 3600 }, // 1時間ごとに再検証 }); return <ProductList products={data} />;}
// 3. ビルド時に生成(SSG)export default async function BlogPost({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { revalidate: false }, // キャッシュを無効化しない }); return <Article post={post} />;}
// 4. タグベースの再検証export default async function Product({ params }: { params: { id: string } }) { const product = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 3600, tags: ['products'] // タグでキャッシュを管理 }, }); return <ProductDetails product={product} />;}
// タグベースの再検証API// app/api/revalidate/route.tsimport { revalidateTag } from 'next/cache';
export async function POST(request: Request) { const { tag } = await request.json(); revalidateTag(tag); // 特定のタグのキャッシュを無効化 return Response.json({ revalidated: true });}画像最適化の深い理解
Section titled “画像最適化の深い理解”Next.js Imageコンポーネントの動作
Section titled “Next.js Imageコンポーネントの動作”画像最適化のメカニズム:
// Next.js Imageコンポーネントが行う最適化:// 1. 自動的な画像フォーマットの変換(WebP、AVIF)// 2. レスポンシブ画像の生成(複数のサイズ)// 3. Lazy Loading(デフォルト)// 4. レイアウトシフトの防止
import Image from 'next/image';
export default function ProductImage({ src, alt }: { src: string; alt: string }) { return ( <Image src={src} alt={alt} width={800} height={600} priority // LCP要素の場合 placeholder="blur" // ブラー効果でレイアウトシフトを防止 blurDataURL="data:image/jpeg;base64,..." // プレースホルダー /> );}
// 画像最適化の設定(next.config.js)module.exports = { images: { formats: ['image/avif', 'image/webp'], // 優先フォーマット deviceSizes: [640, 750, 828, 1080, 1200, 1920], // デバイスサイズ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // 画像サイズ minimumCacheTTL: 60, // キャッシュの最小TTL },};Next.jsアプリケーションのパフォーマンスの深い理解において重要なポイント:
- Core Web Vitals: LCP、FID、CLSの根本的な理解
- バンドルサイズ: コード分割とTree Shakingの最適化
- キャッシング: 多層的なキャッシング戦略の活用
- 画像最適化: Next.js Imageコンポーネントの適切な使用
シニアエンジニアとして考慮すべき点:
- 計測が最優先: 推測ではなく、実際のパフォーマンスデータに基づいて判断
- ユーザー体験: パフォーマンス指標だけでなく、実際のユーザー体験を考慮
- トレードオフ: パフォーマンスと開発体験のバランス
- 継続的な改善: 一度の最適化ではなく、継続的な監視と改善
パフォーマンスの問題は、表面的な症状ではなく、根本原因を理解することで解決できます。