Engineer Quiz

← 記事一覧

Webパフォーマンス最適化 - Code Splitting, Critical Rendering Path

Webパフォーマンス最適化 - Code Splitting, Critical Rendering Path

📚 概要

Webパフォーマンスは、ユーザー体験に直結する重要な要素です。ページの読み込み速度が1秒遅れるだけで、コンバージョン率が7%低下すると言われています。

この記事では、Code SplittingCritical Rendering PathCSS 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

ステップ:

  1. HTML → DOM: HTMLをパースしてDOM Tree を構築
  2. CSS → CSSOM: CSSをパースしてCSSOM Tree を構築
  3. Render Tree: DOM と CSSOM を結合
  4. Layout: 要素の位置とサイズを計算
  5. Paint: ピクセルを描画
  6. 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:#51cf66

4. 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スコアを測定し、改善施策を実施してみましょう!

参考リソース: