Webパフォーマンス最適化 - Code Splitting, Critical Rendering Path
📚 概要
Webパフォーマンスは、ユーザー体験に直結する重要な要素です。ページの読み込み速度が1秒遅れるだけで、コンバージョン率が7%低下すると言われています。
この記事では、Code Splitting、Critical Rendering Path、CSS Containment など、Webパフォーマンス最適化の高度な手法を詳しく解説します。
🕰️ 歴史的背景
Webパフォーマンスの進化
2000年代: 画像最適化の時代
- GIF → JPEG/PNG → WebP への移行
- CSS Sprites でHTTPリクエスト削減
2010年代: JavaScript最適化
- 2015年: Webpack の普及 → バンドル最適化
- 2016年: HTTP/2 導入 → 多重化で並列リクエスト可能に
- 2017年: Code Splitting の標準化
2020年代: Core Web Vitals
- 2020年: Google が Core Web Vitals を発表
- LCP (Largest Contentful Paint): 最大コンテンツの表示時間
- FID (First Input Delay): 最初の入力遅延
- CLS (Cumulative Layout Shift): レイアウトシフト
- 2024年: INP (Interaction to Next Paint) が FID に置き換わる
🔧 技術解説
1. Code Splitting - コード分割
Code Splittingは、JavaScriptバンドルを複数のチャンクに分割し、必要なときだけ読み込む技術です。
なぜ必要?
graph TB
A[Without Code Splitting] --> B[Single Bundle: 2MB]
B --> C[Download All]
C --> D[Parse All]
D --> E[Execute All]
E --> F[Interactive]
G[With Code Splitting] --> H[Entry: 200KB]
H --> I[Download Entry]
I --> J[Parse Entry]
J --> K[Execute Entry]
K --> L[Interactive Faster]
L --> M[Load Lazy Chunks]
style A fill:#ff6b6b
style G fill:#51cf66効果:
- 初期ロード時間の短縮
- メモリ使用量の削減
- キャッシュの効率化
実装方法
1. Dynamic Import(推奨)
// Before: すべてを最初に読み込む
import HeavyComponent from './HeavyComponent';
function App() {
return <HeavyComponent />;
}
// After: 必要なときだけ読み込む
import React, { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
2. Route-based Code Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// ルートごとに分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
3. Component-based Code Splitting
// モーダルは開いたときだけ読み込む
import { useState, lazy, Suspense } from 'react';
const Modal = lazy(() => import('./Modal'));
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<Suspense fallback={<div>Loading...</div>}>
<Modal onClose={() => setIsOpen(false)} />
</Suspense>
)}
</>
);
}
4. Library Code Splitting(Webpack)
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// node_modules を別チャンクに
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors'
},
// 共通コードを別チャンクに
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
name: 'common'
}
}
}
}
};
Prefetching と Preloading
// Prefetch: アイドル時に読み込み(優先度低)
const AdminPanel = lazy(() =>
import(/* webpackPrefetch: true */ './AdminPanel')
);
// Preload: すぐに読み込み(優先度高)
const CriticalComponent = lazy(() =>
import(/* webpackPreload: true */ './CriticalComponent')
);
2. Critical Rendering Path - クリティカルレンダリングパス
Critical Rendering Pathは、ブラウザがHTMLをピクセルに変換するまでの一連のステップです。
レンダリングプロセス
graph LR
A[HTML] --> B[DOM Tree]
C[CSS] --> D[CSSOM Tree]
B --> E[Render Tree]
D --> E
E --> F[Layout]
F --> G[Paint]
G --> H[Composite]
style A fill:#4dabf7
style C fill:#51cf66
style H fill:#ff6b6bステップ:
- HTML → DOM: HTMLをパースしてDOM Tree を構築
- CSS → CSSOM: CSSをパースしてCSSOM Tree を構築
- Render Tree: DOM と CSSOM を結合
- Layout: 要素の位置とサイズを計算
- Paint: ピクセルを描画
- Composite: レイヤーを合成
最適化手法
1. CSS の最適化
<!-- ❌ 悪い例: レンダリングブロッキング -->
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="print.css">
<!-- ✅ 良い例: メディアクエリで条件分岐 -->
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="print.css" media="print">
2. Critical CSS のインライン化
<!DOCTYPE html>
<html>
<head>
<!-- Above-the-fold CSS をインライン -->
<style>
/* ファーストビューに必要な最小限のCSS */
body { margin: 0; font-family: sans-serif; }
.header { background: #333; color: white; padding: 20px; }
.hero { height: 100vh; background: url(hero.jpg); }
</style>
<!-- 残りのCSSは非同期で読み込み -->
<link rel="preload" href="full-styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="full-styles.css"></noscript>
</head>
<body>
<!-- ... -->
</body>
</html>
3. JavaScript の非同期読み込み
<!-- ❌ 悪い例: パーサーブロッキング -->
<script src="script.js"></script>
<!-- ✅ 良い例1: async(順序不問) -->
<script async src="analytics.js"></script>
<!-- ✅ 良い例2: defer(DOMContentLoaded前に実行) -->
<script defer src="main.js"></script>
async vs defer:
graph TB
subgraph Normal
A1[HTML Parse] --> B1[Script Download]
B1 --> C1[Script Execute]
C1 --> D1[Continue Parse]
end
subgraph Async
A2[HTML Parse] --> B2[Script Download]
A2 --> C2[Continue Parse]
B2 --> D2[Script Execute]
D2 --> E2[Continue Parse]
end
subgraph Defer
A3[HTML Parse] --> B3[Script Download]
A3 --> C3[Continue Parse]
C3 --> D3[Script Execute]
end
style Normal fill:#ff6b6b
style Async fill:#ffd43b
style Defer fill:#51cf664. Preconnect と DNS Prefetch
<!-- DNS解決を事前に実行 -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!-- TCP接続まで事前に確立 -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<!-- リソースを事前に読み込み -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
3. CSS Containment - レイアウト最適化
CSS contain プロパティは、要素のレイアウト計算を最適化します。
contain の種類
/* layout: レイアウト計算を分離 */
.card {
contain: layout;
}
/* paint: 描画範囲を制限 */
.hero {
contain: paint;
}
/* size: サイズ計算を分離 */
.fixed-size {
contain: size;
}
/* 全て適用 */
.isolated {
contain: strict; /* layout + paint + size */
}
/* 推奨(sizeを除く) */
.recommended {
contain: content; /* layout + paint */
}
実践例
// 仮想スクロールリストの最適化
function VirtualList({ items }) {
return (
<div style={{ contain: 'strict', height: '500px', overflow: 'auto' }}>
{items.map(item => (
<div
key={item.id}
style={{ contain: 'content', height: '50px' }}
>
{item.content}
</div>
))}
</div>
);
}
💡 実践例: パフォーマンス最適化チェックリスト
1. 初期ロード最適化
// ✅ Code Splitting適用
const routes = [
{
path: '/',
component: lazy(() => import('./pages/Home'))
},
{
path: '/dashboard',
component: lazy(() => import('./pages/Dashboard'))
}
];
// ✅ Critical CSS抽出
// package.json
{
"scripts": {
"build:critical": "critical index.html --base build/ --inline --minify"
}
}
// ✅ 画像最適化
<picture>
<source srcSet="hero.webp" type="image/webp">
<source srcSet="hero.jpg" type="image/jpeg">
<img src="hero.jpg" alt="Hero" loading="lazy" />
</picture>
2. ランタイムパフォーマンス
// ✅ React.memo でメモ化
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* 重い処理 */}</div>;
});
// ✅ useMemo で計算結果をキャッシュ
const sortedData = useMemo(() => {
return [...data].sort((a, b) => a.value - b.value);
}, [data]);
// ✅ useCallback でイベントハンドラをメモ化
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
// ✅ Intersection Observer で遅延ロード
const LazyImage = ({ src, alt }) => {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
});
observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isVisible ? src : 'placeholder.jpg'}
alt={alt}
/>
);
};
3. Lighthouse によるスコア改善
# Lighthouse CLI でスコアチェック
npm install -g lighthouse
lighthouse https://example.com --view
# CI/CD で自動チェック
lighthouse https://example.com --output json --output-path ./report.json
目標スコア:
- Performance: 90+
- Accessibility: 90+
- Best Practices: 90+
- SEO: 90+
📊 パフォーマンス指標
Core Web Vitals
| 指標 | 良い | 改善が必要 | 悪い |
|---|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | 2.5-4s | > 4s |
| INP (Interaction to Next Paint) | < 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | < 0.1 | 0.1-0.25 | > 0.25 |
その他の重要指標
- TTFB (Time to First Byte): < 600ms
- FCP (First Contentful Paint): < 1.8s
- TTI (Time to Interactive): < 3.8s
🎯 ベストプラクティス
1. バンドルサイズの監視
// package.json
{
"scripts": {
"analyze": "webpack-bundle-analyzer dist/stats.json"
}
}
# バンドルサイズをチェック
npm run build
npm run analyze
2. Tree Shaking
// ❌ 悪い例: 全体をインポート
import _ from 'lodash';
const result = _.debounce(fn, 300);
// ✅ 良い例: 必要な関数だけインポート
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
3. Service Worker でキャッシュ
// service-worker.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
🔍 関連する問題
この記事に関連するクイズ問題:
- Q10: Code Splitting の目的
- Q20: Critical Rendering Path の最適化
- Q27: CSS contain プロパティの目的
📝 まとめ
- Code Splitting: バンドルを分割して初期ロードを高速化
- Critical Rendering Path: レンダリングプロセスを最適化
- CSS Containment: レイアウト計算を効率化
- Core Web Vitals: LCP, INP, CLS を改善
- 継続的な監視: Lighthouse, Bundle Analyzer で定期チェック
次のステップ: 実際のプロジェクトでLighthouseスコアを測定し、改善施策を実施してみましょう!
参考リソース: