CSS実践入門第9回:パフォーマンス最適化 - 高速読み込みとレンダリング最適化

CSS・フロントエンドのパフォーマンス最適化手法を詳しく解説。Critical CSS、画像最適化、レンダリングブロッキングの回避など、高速なWebサイトを実現するテクニックを紹介します。

Webサイトのパフォーマンスは、ユーザーエクスペリエンスとSEOの両面で極めて重要な要素です。今回は、CSS・フロントエンド技術を活用したパフォーマンス最適化の実践的な手法を詳しく解説し、高速で快適なWebサイトを構築するテクニックを紹介します。

この記事で学べること

  • Critical CSS による初期レンダリング高速化
  • 画像最適化と遅延読み込みの実装
  • レンダリングブロッキングの回避技術
  • CSS・JavaScript バンドルサイズの最適化
  • Core Web Vitals の改善手法

1. Critical CSS の実装

Critical CSS の基本概念

Critical CSS は、ファーストビューの表示に必要な最小限のCSSのみを抽出し、HTMLに直接埋め込む手法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>パフォーマンス最適化サイト</title>
    
    <!-- Critical CSS をインライン挿入 -->
    <style>
        /* ファーストビュー用の最小限CSS */
        :root {
            --primary-color: #3498db;
            --text-color: #2c3e50;
            --background-color: #ffffff;
        }
        
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 16px;
            line-height: 1.6;
            color: var(--text-color);
            background-color: var(--background-color);
        }
        
        .header {
            background: var(--primary-color);
            color: white;
            padding: 1rem 0;
            position: sticky;
            top: 0;
            z-index: 100;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 0 1rem;
        }
        
        .hero {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 4rem 0;
            text-align: center;
        }
        
        .hero h1 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
            font-weight: 700;
        }
        
        .hero p {
            font-size: 1.2rem;
            margin-bottom: 2rem;
            opacity: 0.9;
        }
    </style>
    
    <!-- メインCSS を非同期読み込み -->
    <link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>
<body>
    <header class="header">
        <div class="container">
            <nav><!-- ナビゲーション --></nav>
        </div>
    </header>
    
    <section class="hero">
        <div class="container">
            <h1>高速Webサイト</h1>
            <p>パフォーマンス最適化の実践</p>
        </div>
    </section>
    
    <!-- 残りのコンテンツ -->
</body>
</html>

自動 Critical CSS 抽出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Critical CSS 抽出ツール(Node.js)
const critical = require('critical');
const path = require('path');

async function generateCriticalCSS() {
    try {
        await critical.generate({
            // ベースとなるHTML
            base: 'dist/',
            src: 'index.html',
            dest: 'index-critical.html',
            
            // CSS ファイル
            css: ['css/main.css'],
            
            // ビューポートサイズ
            dimensions: [
                {
                    height: 900,
                    width: 1300
                },
                {
                    height: 720,
                    width: 414
                }
            ],
            
            // 抽出設定
            penthouse: {
                blockJSRequests: false,
            },
            
            // インライン挿入
            inline: true,
            
            // 元のCSS削除
            extract: true,
            
            // 最小化
            minify: true,
            
            // 無視するCSS
            ignore: {
                atrule: ['@font-face'],
                rule: [/some-unused-class/],
                decl: (node, value) => {
                    return /\.sr-only/.test(value);
                }
            }
        });
        
        console.log('Critical CSS generated successfully!');
    } catch (error) {
        console.error('Critical CSS generation failed:', error);
    }
}

generateCriticalCSS();

2. 画像最適化とレスポンシブ画像

次世代画像フォーマット対応

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- WebP/AVIF 対応のレスポンシブ画像 -->
<picture>
    <source 
        srcset="image-320.avif 320w, image-640.avif 640w, image-960.avif 960w, image-1280.avif 1280w"
        sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
        type="image/avif">
    <source 
        srcset="image-320.webp 320w, image-640.webp 640w, image-960.webp 960w, image-1280.webp 1280w"
        sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
        type="image/webp">
    <img 
        src="image-640.jpg"
        srcset="image-320.jpg 320w, image-640.jpg 640w, image-960.jpg 960w, image-1280.jpg 1280w"
        sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
        alt="最適化された画像"
        loading="lazy"
        decoding="async">
</picture>

画像遅延読み込みの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 遅延読み込み画像の初期状態 */
.lazy-image {
    opacity: 0;
    transition: opacity 0.3s ease;
    background-color: #f0f0f0;
}

.lazy-image.loaded {
    opacity: 1;
}

/* スケルトンローダー */
.image-skeleton {
    background: linear-gradient(
        90deg,
        #f0f0f0 25%,
        #e0e0e0 50%,
        #f0f0f0 75%
    );
    background-size: 200% 100%;
    animation: skeleton-loading 1.5s infinite;
}

@keyframes skeleton-loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ぼかしエフェクト付きプレースホルダー */
.lazy-blur {
    filter: blur(5px);
    transform: scale(1.05);
    transition: all 0.3s ease;
}

.lazy-blur.loaded {
    filter: blur(0);
    transform: scale(1);
}

Intersection Observer による遅延読み込み

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class LazyImageLoader {
    constructor(options = {}) {
        this.options = {
            rootMargin: '50px 0px',
            threshold: 0.01,
            enableBlur: true,
            ...options
        };
        
        this.imageObserver = new IntersectionObserver(
            this.handleImageIntersection.bind(this),
            {
                rootMargin: this.options.rootMargin,
                threshold: this.options.threshold
            }
        );
        
        this.init();
    }
    
    init() {
        const lazyImages = document.querySelectorAll('img[data-src]');
        lazyImages.forEach(img => {
            this.setupImage(img);
            this.imageObserver.observe(img);
        });
    }
    
    setupImage(img) {
        img.classList.add('lazy-image');
        
        // 低品質プレースホルダー
        if (img.dataset.placeholder) {
            img.src = img.dataset.placeholder;
            if (this.options.enableBlur) {
                img.classList.add('lazy-blur');
            }
        } else {
            img.classList.add('image-skeleton');
        }
    }
    
    handleImageIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                this.loadImage(entry.target);
                this.imageObserver.unobserve(entry.target);
            }
        });
    }
    
    async loadImage(img) {
        const src = img.dataset.src;
        const srcset = img.dataset.srcset;
        
        try {
            // 画像プリロード
            const imageLoader = new Image();
            
            if (srcset) {
                imageLoader.srcset = srcset;
            }
            imageLoader.src = src;
            
            // 画像読み込み完了を待つ
            await new Promise((resolve, reject) => {
                imageLoader.onload = resolve;
                imageLoader.onerror = reject;
            });
            
            // 画像を適用
            if (srcset) {
                img.srcset = srcset;
            }
            img.src = src;
            
            // ロード完了時の処理
            img.onload = () => {
                img.classList.add('loaded');
                img.classList.remove('lazy-blur', 'image-skeleton');
            };
            
        } catch (error) {
            console.error('Image loading failed:', error);
            img.classList.add('image-error');
        }
    }
}

// 初期化
document.addEventListener('DOMContentLoaded', () => {
    new LazyImageLoader({
        rootMargin: '100px 0px',
        enableBlur: true
    });
});

3. レンダリング最適化

レイアウトシフトの最小化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/* アスペクト比によるレイアウト予約 */
.aspect-ratio {
    position: relative;
    width: 100%;
}

.aspect-ratio::before {
    content: '';
    display: block;
    padding-top: var(--aspect-ratio, 56.25%); /* 16:9 = 56.25% */
}

.aspect-ratio > * {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* 様々なアスペクト比 */
.aspect-16-9 { --aspect-ratio: 56.25%; }
.aspect-4-3 { --aspect-ratio: 75%; }
.aspect-1-1 { --aspect-ratio: 100%; }
.aspect-3-2 { --aspect-ratio: 66.67%; }

/* CSS aspect-ratio プロパティ(モダンブラウザ) */
.modern-aspect {
    aspect-ratio: 16 / 9;
    width: 100%;
    height: auto;
}

/* フォント読み込み中のレイアウトシフト対策 */
@font-face {
    font-family: 'CustomFont';
    src: url('custom-font.woff2') format('woff2');
    font-display: swap; /* フォールバックフォントを即座に表示 */
    size-adjust: 100%; /* サイズ調整でレイアウトシフトを最小化 */
}

/* システムフォントスタック */
.font-system {
    font-family: 
        -apple-system,
        BlinkMacSystemFont,
        'Segoe UI',
        'Roboto',
        'Oxygen',
        'Ubuntu',
        'Cantarell',
        'Fira Sans',
        'Droid Sans',
        'Helvetica Neue',
        sans-serif;
}

GPU アクセラレーションの活用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* GPU で処理されるプロパティを優先使用 */
.gpu-optimized {
    /* transform, opacity は GPU で高速処理 */
    transform: translateZ(0); /* ハードウェアアクセラレーション強制 */
    will-change: transform, opacity;
}

/* 避けるべき重いプロパティ */
.cpu-heavy {
    /* これらは CPU で処理され、重い */
    /* width, height, top, left, margin, padding, border-width */
}

/* transform を使った代替実装 */
.efficient-animations {
    /* width の代わりに scaleX */
    transform: scaleX(0.8);
    
    /* position の代わりに translate */
    transform: translateX(100px) translateY(50px);
    
    /* 複数の transform を組み合わせ */
    transform: translateX(100px) rotate(45deg) scale(1.2);
}

/* containment による最適化 */
.contained-component {
    contain: layout style paint;
}

.strictly-contained {
    contain: strict; /* layout + style + paint + size の組み合わせ */
}

4. CSS バンドル最適化

未使用 CSS の削除

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// PurgeCSS 設定例
const purgecss = require('@fullhuman/postcss-purgecss');

module.exports = {
    plugins: [
        purgecss({
            content: [
                './src/**/*.html',
                './src/**/*.js',
                './src/**/*.vue'
            ],
            
            // 保持するクラス(動的に生成される)
            safelist: {
                standard: [
                    'active',
                    'disabled',
                    'loading',
                    /^swiper-/,
                    /^aos-/
                ],
                deep: [/^modal-/, /^tooltip-/],
                greedy: [/^ripple-/]
            },
            
            // カスタム抽出器
            extractors: [
                {
                    extractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
                    extensions: ['html', 'js', 'vue']
                }
            ],
            
            // 削除しないセレクター
            rejected: false,
            printRejected: false
        }),
        
        // CSS 最小化
        require('cssnano')({
            preset: [
                'default',
                {
                    discardComments: {
                        removeAll: true
                    }
                }
            ]
        })
    ]
};

CSS の分割と遅延読み込み

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* コンポーネント別 CSS 分割 */

/* critical.css - ファーストビュー用 */
.header, .nav, .hero, .container {
    /* 重要なスタイル */
}

/* components.css - コンポーネント用 */
.button, .card, .modal, .dropdown {
    /* コンポーネントスタイル */
}

/* utilities.css - ユーティリティ用 */
.text-center, .mb-4, .d-flex, .justify-center {
    /* ユーティリティスタイル */
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// CSS の動的読み込み
class CSSLoader {
    static loadedStylesheets = new Set();
    
    static async loadCSS(href, media = 'all') {
        if (this.loadedStylesheets.has(href)) {
            return;
        }
        
        return new Promise((resolve, reject) => {
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = href;
            link.media = media;
            
            link.onload = () => {
                this.loadedStylesheets.add(href);
                resolve();
            };
            
            link.onerror = reject;
            
            document.head.appendChild(link);
        });
    }
    
    // コンポーネント使用時の CSS 読み込み
    static async loadComponentCSS(componentName) {
        const cssMap = {
            'modal': '/css/components/modal.css',
            'carousel': '/css/components/carousel.css',
            'chart': '/css/components/chart.css'
        };
        
        const cssPath = cssMap[componentName];
        if (cssPath) {
            await this.loadCSS(cssPath);
        }
    }
}

// 使用例
class ModalComponent {
    async show() {
        // モーダル表示前に CSS を読み込み
        await CSSLoader.loadComponentCSS('modal');
        
        // モーダル表示処理
        this.element.classList.add('modal--active');
    }
}

5. JavaScript 最適化

コード分割と遅延読み込み

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 動的インポートによるコード分割
class ComponentLoader {
    static componentCache = new Map();
    
    static async loadComponent(name) {
        if (this.componentCache.has(name)) {
            return this.componentCache.get(name);
        }
        
        let component;
        
        switch (name) {
            case 'search':
                component = await import('./components/SearchComponent.js');
                break;
            case 'chart':
                component = await import('./components/ChartComponent.js');
                break;
            case 'carousel':
                component = await import('./components/CarouselComponent.js');
                break;
            default:
                throw new Error(`Unknown component: ${name}`);
        }
        
        this.componentCache.set(name, component.default);
        return component.default;
    }
}

// Intersection Observer で必要時に読み込み
class LazyComponentLoader {
    constructor() {
        this.componentObserver = new IntersectionObserver(
            this.handleComponentIntersection.bind(this),
            { rootMargin: '100px 0px' }
        );
        
        this.init();
    }
    
    init() {
        document.querySelectorAll('[data-component]').forEach(element => {
            this.componentObserver.observe(element);
        });
    }
    
    async handleComponentIntersection(entries) {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                const element = entry.target;
                const componentName = element.dataset.component;
                
                try {
                    const Component = await ComponentLoader.loadComponent(componentName);
                    new Component(element);
                    
                    this.componentObserver.unobserve(element);
                } catch (error) {
                    console.error(`Failed to load component ${componentName}:`, error);
                }
            }
        }
    }
}

// 初期化
document.addEventListener('DOMContentLoaded', () => {
    new LazyComponentLoader();
});

Service Worker による キャッシュ最適化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// service-worker.js
const CACHE_NAME = 'site-cache-v1';
const STATIC_CACHE_URLS = [
    '/',
    '/css/critical.css',
    '/js/main.js',
    '/images/logo.svg'
];

// インストール時の処理
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(STATIC_CACHE_URLS))
            .then(() => self.skipWaiting())
    );
});

// フェッチ時の処理
self.addEventListener('fetch', event => {
    // CSS・JS・画像の キャッシュ戦略
    if (event.request.destination === 'style' || 
        event.request.destination === 'script' || 
        event.request.destination === 'image') {
        
        event.respondWith(
            caches.match(event.request)
                .then(response => {
                    if (response) {
                        return response;
                    }
                    
                    return fetch(event.request)
                        .then(response => {
                            const responseClone = response.clone();
                            caches.open(CACHE_NAME)
                                .then(cache => {
                                    cache.put(event.request, responseClone);
                                });
                            return response;
                        });
                })
        );
    }
});

// メインスレッドでの Service Worker 登録
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
            .then(registration => {
                console.log('SW registered: ', registration);
            })
            .catch(registrationError => {
                console.log('SW registration failed: ', registrationError);
            });
    });
}

6. パフォーマンス測定と監視

Web Vitals の測定

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Core Web Vitals の測定
import {getLCP, getFID, getCLS, getFCP, getTTFB} from 'web-vitals';

class PerformanceMonitor {
    constructor(options = {}) {
        this.options = {
            reportEndpoint: '/api/performance',
            sampleRate: 0.1, // 10%のサンプリング
            ...options
        };
        
        this.metrics = {};
        this.init();
    }
    
    init() {
        // サンプリング判定
        if (Math.random() > this.options.sampleRate) {
            return;
        }
        
        // Core Web Vitals の測定
        getLCP(this.handleMetric.bind(this, 'LCP'));
        getFID(this.handleMetric.bind(this, 'FID'));
        getCLS(this.handleMetric.bind(this, 'CLS'));
        getFCP(this.handleMetric.bind(this, 'FCP'));
        getTTFB(this.handleMetric.bind(this, 'TTFB'));
        
        // ページアンロード時にレポート送信
        window.addEventListener('beforeunload', () => {
            this.sendReport();
        });
    }
    
    handleMetric(name, metric) {
        this.metrics[name] = metric;
        
        // リアルタイム分析
        this.analyzeMetric(name, metric);
    }
    
    analyzeMetric(name, metric) {
        const thresholds = {
            LCP: { good: 2500, poor: 4000 },
            FID: { good: 100, poor: 300 },
            CLS: { good: 0.1, poor: 0.25 },
            FCP: { good: 1800, poor: 3000 },
            TTFB: { good: 800, poor: 1800 }
        };
        
        const threshold = thresholds[name];
        let rating = 'good';
        
        if (metric.value > threshold.poor) {
            rating = 'poor';
        } else if (metric.value > threshold.good) {
            rating = 'needs-improvement';
        }
        
        console.log(`${name}: ${metric.value} (${rating})`);
        
        // パフォーマンス問題の警告
        if (rating === 'poor') {
            this.reportPerformanceIssue(name, metric);
        }
    }
    
    reportPerformanceIssue(name, metric) {
        // 開発者向け警告
        console.warn(`Performance issue detected: ${name} = ${metric.value}`);
        
        // 改善提案
        const suggestions = {
            LCP: 'Consider optimizing images, reducing server response time, or implementing Critical CSS',
            FID: 'Consider reducing JavaScript execution time or implementing code splitting',
            CLS: 'Ensure images and ads have dimensions set, and avoid inserting content above existing content',
            FCP: 'Consider reducing render-blocking resources or implementing Critical CSS',
            TTFB: 'Consider optimizing server response time or using a CDN'
        };
        
        console.info(`Suggestion for ${name}: ${suggestions[name]}`);
    }
    
    async sendReport() {
        if (Object.keys(this.metrics).length === 0) {
            return;
        }
        
        const reportData = {
            url: window.location.href,
            userAgent: navigator.userAgent,
            connectionType: navigator.connection?.effectiveType || 'unknown',
            metrics: this.metrics,
            timestamp: Date.now()
        };
        
        try {
            // Beacon API で確実に送信
            if (navigator.sendBeacon) {
                navigator.sendBeacon(
                    this.options.reportEndpoint,
                    JSON.stringify(reportData)
                );
            } else {
                // フォールバック
                await fetch(this.options.reportEndpoint, {
                    method: 'POST',
                    body: JSON.stringify(reportData),
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    keepalive: true
                });
            }
        } catch (error) {
            console.error('Failed to send performance report:', error);
        }
    }
}

// 初期化
document.addEventListener('DOMContentLoaded', () => {
    new PerformanceMonitor({
        sampleRate: 0.1,
        reportEndpoint: '/api/performance'
    });
});

バンドル分析とビルド最適化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// webpack.config.js での最適化例
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
    mode: 'production',
    
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                },
                common: {
                    minChunks: 2,
                    chunks: 'all',
                    enforce: true
                }
            }
        }
    },
    
    plugins: [
        // CSS 抽出
        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash].css',
            chunkFilename: 'css/[id].[contenthash].css'
        }),
        
        // バンドル分析
        new BundleAnalyzerPlugin({
            analyzerMode: process.env.ANALYZE ? 'server' : 'disabled'
        }),
        
        // Gzip 圧縮
        new CompressionPlugin({
            algorithm: 'gzip',
            test: /\.(js|css|html|svg)$/,
            threshold: 8192,
            minRatio: 0.8
        })
    ]
};

まとめ

パフォーマンス最適化は、ユーザーエクスペリエンスの向上とビジネス成果の直結する重要な要素です。

主要な最適化ポイント

  • Critical CSS: ファーストビューの高速表示
  • 画像最適化: 次世代フォーマットと遅延読み込み
  • レンダリング最適化: レイアウトシフトの最小化
  • バンドル最適化: 未使用コードの削除と分割読み込み
  • キャッシュ戦略: Service Worker による効率的なキャッシュ

継続的な改善プロセス

  • パフォーマンス指標の定期的な測定
  • Core Web Vitals の監視と改善
  • ユーザーフィードバックの収集と分析
  • 新技術・手法の継続的な導入

開発ワークフローへの統合

  • ビルド時の自動最適化
  • パフォーマンス回帰の検出
  • ステージング環境でのパフォーマンステスト
  • 本番環境でのリアルタイム監視

パフォーマンス最適化は一度きりの作業ではなく、継続的な改善プロセスです。ユーザーの期待値の向上と新技術の登場に合わせて、常に最新の最適化手法を取り入れていくことが重要です。

次回の記事では、「CSS実践入門第10回:SEO・アクセシビリティ」について解説予定です。検索エンジンに最適化され、すべてのユーザーにアクセシブルなWebサイトの構築方法を詳しく紹介します。


関連記事:

技術ネタ、趣味や備忘録などを書いているブログです
Hugo で構築されています。
テーマ StackJimmy によって設計されています。