「60fpsを維持するには、各フレームを16.67ms以内に処理する必要がある」と佐藤CTOは言った。「ブラウザのレンダリングパイプラインを理解すれば、何を最適化すべきか見えてくる。」
1. Critical Rendering Path
HTML → DOM → CSSOM → Render Tree → Layout → Paint → Composite
↑
Blocking CSS/JS
レンダリングブロックの排除
<!-- CSS: クリティカルCSSをインラインに、残りは非同期ロード -->
<style>/* Critical CSS inline */</style>
<link rel="preload" href="/styles/main.css" as="style"
onload="this.rel='stylesheet'" />
<!-- JS: defer または async -->
<script src="/app.js" defer></script>
<!-- defer: DOMパース完了後に実行(順序保証あり) -->
<!-- async: ダウンロード完了次第実行(順序保証なし) -->
2. SSR / SSG / ISR
| 戦略 | TTFB | LCP | インタラクティブ | 適用場面 |
|---|---|---|---|---|
| CSR | 速い | 遅い | 遅い | SPA(管理画面) |
| SSR | やや遅い | 速い | やや遅い | 動的コンテンツ |
| SSG | 速い | 速い | 速い | 静的コンテンツ |
| ISR | 速い | 速い | 速い | 定期更新コンテンツ |
// Next.js の ISR(Incremental Static Regeneration)
export async function getStaticProps() {
const products = await fetchProducts();
return {
props: { products },
revalidate: 60, // 60秒ごとに再生成
};
}
// App Router の場合
export const revalidate = 60;
export default async function ProductsPage() {
const products = await fetchProducts();
return <ProductList products={products} />;
}
3. Hydration 最適化
// Partial Hydration: インタラクティブな部分だけ Hydrate
// Astro の islands アーキテクチャ
// <Counter client:visible /> → ビューポートに入ったら Hydrate
// <Search client:idle /> → メインスレッドがアイドル時に Hydrate
// <Newsletter client:load /> → ページロード時に即 Hydrate
// React Server Components(RSC)
// サーバーコンポーネント: JS バンドルに含まれない
// クライアントコンポーネント: 'use client' を付与
// Progressive Hydration パターン
import { lazy, Suspense } from 'react';
// 画面外のコンポーネントは遅延Hydrate
const Comments = lazy(() => import('./Comments'));
const RelatedProducts = lazy(() => import('./RelatedProducts'));
function ProductPage({ product }: { product: Product }) {
return (
<>
{/* 即座にインタラクティブ */}
<ProductDetail product={product} />
<AddToCartButton productId={product.id} />
{/* スクロール時にHydrate */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments productId={product.id} />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts categoryId={product.categoryId} />
</Suspense>
</>
);
}
4. Virtual DOM 最適化
// React の re-render を最小化する
// 1. memo で不要な再レンダリングを防止
const ProductCard = React.memo(function ProductCard({ product }: { product: Product }) {
return <div>{product.name} - ¥{product.price}</div>;
});
// 2. useMemo で計算結果をキャッシュ
function ProductList({ products, filter }: Props) {
const filtered = useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}
// 3. リスト仮想化で大量のDOM要素を削減
import { FixedSizeList } from 'react-window';
function VirtualList({ items }: { items: Product[] }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={80}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ProductCard product={items[index]} />
</div>
)}
</FixedSizeList>
);
}
5. Web Worker 活用
// 重い計算をメインスレッドから分離
// worker.ts
self.onmessage = (e: MessageEvent<{ products: Product[]; query: string }>) => {
const { products, query } = e.data;
// 重い検索・ソート処理をワーカーで実行
const results = products
.filter(p => p.name.includes(query) || p.description.includes(query))
.sort((a, b) => calculateRelevance(b, query) - calculateRelevance(a, query));
self.postMessage({ results });
};
// main.ts
const searchWorker = new Worker(new URL('./worker.ts', import.meta.url));
function searchProducts(query: string) {
searchWorker.postMessage({ products: allProducts, query });
searchWorker.onmessage = (e) => {
setSearchResults(e.data.results);
};
}
まとめ
| トピック | 要点 |
|---|---|
| Critical Rendering Path | CSS/JSのブロッキングを排除し初期描画を高速化 |
| SSR/SSG/ISR | コンテンツ特性に応じたレンダリング戦略を選択 |
| Hydration最適化 | Partial/Progressive Hydrationで不要なJSを削減 |
| Virtual DOM | memo, useMemo, リスト仮想化で再レンダリングを最小化 |
| Web Worker | 重い計算をメインスレッドから分離しINPを改善 |
チェックリスト
- Critical Rendering Pathのボトルネックを特定できる
- SSR/SSG/ISRの使い分けを判断できる
- Hydration最適化の手法を知っている
- React.memo / useMemo / react-window を活用できる
- Web Workerで重い処理を分離できる
次のステップへ
レンダリングパフォーマンスを学んだ。次は 演習 で、フロントエンドの最適化を実践しよう。
推定読了時間: 30分