PageSpeed Insightsの「JavaScriptの実行にかかる時間」って何?仕組みから改善策まで紹介

13 min 10 views

ウェブサイトの表示速度が遅い…ユーザーが離脱してしまう…そんな悩みを抱えていませんか?実は、現代のウェブサイトでパフォーマンスボトルネックとなる最大の要因の一つが「JavaScript の実行時間」なんです。

私たちlandinghubでは、数多くのウェブサイトの表示速度改善を手がけてきました。その経験から言えることは、JavaScript の実行時間を最適化することで、ページの読み込み速度を劇的に改善できるということです。

この記事では、JavaScript の実行時間に関する問題から具体的な解決策まで、初心者の方でも理解できるよう詳しく解説していきます。明日からすぐに実践できる内容ばかりですので、ぜひ最後までお読みください。

目次

JavaScript 実行時間とは?基本概念の理解

JavaScript の実行モデル

まず、JavaScript がブラウザでどのように動作するかを理解することが重要です。JavaScript は基本的にシングルスレッドで動作します。つまり、一つのメインスレッド(UIスレッド)で処理が行われるということです。

このメインスレッドでは、JavaScript の実行だけでなく、以下の処理も同時に行われます:

  • DOM の操作
  • レイアウトの計算
  • レンダリング
  • ユーザーイベントの処理

そのため、JavaScript の実行に時間がかかると、他の処理も遅延し、結果的にユーザー体験が悪化してしまうんです。

イベントループの仕組み

JavaScript の実行は「イベントループ」というモデルで管理されています。処理はタスクキューに格納され、順番に実行されます。長時間実行される処理(50ミリ秒以上)は「Long Task」と呼ばれ、ユーザーの操作への応答性を著しく低下させます。

JavaScript 実行時間に影響を与える要因

1. コードの効率性

効率的でないアルゴリズムやデータ構造の使用は、実行時間を大幅に増加させます。例えば:

Copy// 非効率な例
function findUser(users, id) {
    for (let i = 0; i < users.length; i++) {
        if (users[i].id === id) {
            return users[i];
        }
    }
}

// 効率的な例(Map を使用)
const userMap = new Map();
users.forEach(user => userMap.set(user.id, user));
function findUser(id) {
    return userMap.get(id);
}

2. DOM 操作の頻度

DOM 操作は非常にコストが高い処理です。頻繁な DOM 操作は実行時間を大幅に増加させます。

3. メモリ使用量とガベージコレクション

不適切なメモリ管理により、ガベージコレクション(GC)が頻繁に発生すると、JavaScript の実行が一時停止し、パフォーマンスが低下します。

4. ブラウザの処理能力

使用しているブラウザや端末の性能も実行時間に大きく影響します。特にモバイル端末では、デスクトップと比較して処理能力が限られています。

JavaScript 実行時間を測定する方法

Chrome DevTools を使った測定

最も基本的で効果的な方法は、Chrome DevTools の Performance パネルを使用することです:

  1. Chrome DevTools を開く(F12)
  2. Performance タブを選択
  3. 記録ボタンをクリック
  4. サイトの操作を行う
  5. 記録を停止して結果を分析

これにより、どの処理に時間がかかっているかを視覚的に確認できます。

console.time() を使った簡易測定

Copyconsole.time('処理時間測定');
// 測定したい処理
console.timeEnd('処理時間測定');

パフォーマンス API の活用

Copyconst start = performance.now();
// 測定したい処理
const end = performance.now();
console.log(`実行時間: ${end - start}ミリ秒`);

具体的な最適化テクニック

1. 非同期処理の活用

Promise と async/await の使用

Copy// 同期処理(ブロッキング)
function fetchUserData() {
    const response = fetch('/api/user');
    // この間、他の処理が停止
    return response.json();
}

// 非同期処理(ノンブロッキング)
async function fetchUserData() {
    const response = await fetch('/api/user');
    return response.json();
}

Web Workers の活用

重い計算処理は Web Workers を使用してメインスレッドから分離します:

Copy// main.js
const worker = new Worker('heavy-calculation.js');
worker.postMessage(data);
worker.onmessage = function(e) {
    console.log('計算結果:', e.data);
};

// heavy-calculation.js
self.onmessage = function(e) {
    const result = heavyCalculation(e.data);
    self.postMessage(result);
};

2. DOM 操作の最適化

DocumentFragment の使用

Copy// 非効率な方法
for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    document.getElementById('list').appendChild(li);
}

// 効率的な方法
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);

レイアウトスラッシングの回避

Copy// 悪い例:読み書きが混在
elements.forEach(el => {
    const width = el.offsetWidth; // 読み取り
    el.style.width = (width * 1.1) + 'px'; // 書き込み
});

// 良い例:読み取りと書き込みを分離
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, index) => {
    el.style.width = (widths[index] * 1.1) + 'px';
});

3. 効率的なデータ構造の選択

Map と Set の活用

Copy// 配列での検索(O(n))
const users = [/* 大量のユーザーデータ */];
const targetUser = users.find(user => user.id === targetId);

// Map での検索(O(1))
const userMap = new Map(users.map(user => [user.id, user]));
const targetUser = userMap.get(targetId);

4. メモ化(Memoization)の実装

Copyfunction memoize(fn) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

const expensiveCalculation = memoize((a, b) => {
    // 重い計算処理
    return a * b;
});

5. イベントハンドラーの最適化

Debounce と Throttle の実装

Copy// Debounce実装
function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

// Throttle実装
function throttle(func, delay) {
    let lastExecTime = 0;
    return function(...args) {
        const currentTime = Date.now();
        if (currentTime - lastExecTime >= delay) {
            func.apply(this, args);
            lastExecTime = currentTime;
        }
    };
}

// 使用例
const optimizedScrollHandler = throttle(() => {
    // スクロール処理
}, 100);
window.addEventListener('scroll', optimizedScrollHandler);

長いタスクの分割方法

scheduler.yield() の活用

Copyasync function processLargeArray(array) {
    for (let i = 0; i < array.length; i++) {
        // 処理
        processItem(array[i]);
        
        // 一定間隔でメインスレッドに制御を戻す
        if (i % 100 === 0) {
            await scheduler.yield();
        }
    }
}

requestAnimationFrame の使用

Copyfunction processArrayChunks(array, chunkSize = 100) {
    let index = 0;
    
    function processChunk() {
        const endIndex = Math.min(index + chunkSize, array.length);
        
        for (let i = index; i < endIndex; i++) {
            processItem(array[i]);
        }
        
        index = endIndex;
        
        if (index < array.length) {
            requestAnimationFrame(processChunk);
        }
    }
    
    requestAnimationFrame(processChunk);
}

JavaScript ファイルの読み込み最適化

適切な読み込み方法の選択

1. defer属性の使用(推奨)

Copy<script defer src="main.js"></script>

deferを使用することで、HTML の解析を阻害せずに JavaScript を読み込めます。

2. async属性の使用

Copy<script async src="analytics.js"></script>

トラッキングスクリプトなど、独立性の高い処理には async を使用します。

3. 動的読み込み

Copy// 必要な時にのみ読み込む
async function loadModule() {
    const module = await import('./heavy-module.js');
    return module.default;
}

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

Copy// 初期に必要な処理のみ実行
document.addEventListener('DOMContentLoaded', async () => {
    // 重要な初期化処理
    initializeApp();
    
    // 二次的な機能は遅延読み込み
    setTimeout(async () => {
        const analytics = await import('./analytics.js');
        analytics.init();
    }, 1000);
});

メモリ管理とガベージコレクション対策

メモリリークの防止

Copy// 悪い例:メモリリークを起こしやすい
let globalData = [];
function addData(data) {
    globalData.push(data);
    // globalData が永続的に蓄積される
}

// 良い例:適切なメモリ管理
class DataManager {
    constructor() {
        this.data = [];
    }
    
    addData(data) {
        this.data.push(data);
        // 必要に応じてクリーンアップ
        if (this.data.length > 1000) {
            this.data = this.data.slice(-500);
        }
    }
    
    clear() {
        this.data = [];
    }
}

イベントリスナーの適切な削除

Copy// リスナーの追加
const handler = () => console.log('クリック');
element.addEventListener('click', handler);

// リスナーの削除
element.removeEventListener('click', handler);

実践的な最適化事例

事例1: 大量データの表示最適化

Copy// 仮想スクロールの実装例
class VirtualList {
    constructor(container, items, itemHeight) {
        this.container = container;
        this.items = items;
        this.itemHeight = itemHeight;
        this.visibleItems = Math.ceil(container.offsetHeight / itemHeight) + 2;
        this.scrollTop = 0;
        
        this.render();
        this.bindEvents();
    }
    
    render() {
        const startIndex = Math.floor(this.scrollTop / this.itemHeight);
        const endIndex = Math.min(startIndex + this.visibleItems, this.items.length);
        
        // 表示領域のアイテムのみレンダリング
        const fragment = document.createDocumentFragment();
        for (let i = startIndex; i < endIndex; i++) {
            const item = this.createItem(this.items[i], i);
            fragment.appendChild(item);
        }
        
        this.container.innerHTML = '';
        this.container.appendChild(fragment);
    }
    
    bindEvents() {
        this.container.addEventListener('scroll', 
            throttle(() => {
                this.scrollTop = this.container.scrollTop;
                this.render();
            }, 16)
        );
    }
}

事例2: アニメーション最適化

Copy// requestAnimationFrame を使った最適化
class OptimizedAnimation {
    constructor(element) {
        this.element = element;
        this.isAnimating = false;
    }
    
    animate() {
        if (this.isAnimating) return;
        
        this.isAnimating = true;
        let start = performance.now();
        
        const animateFrame = (timestamp) => {
            const elapsed = timestamp - start;
            const progress = Math.min(elapsed / 1000, 1); // 1秒のアニメーション
            
            // CSS transform を使用(reflow を避ける)
            this.element.style.transform = `translateX(${progress * 100}px)`;
            
            if (progress < 1) {
                requestAnimationFrame(animateFrame);
            } else {
                this.isAnimating = false;
            }
        };
        
        requestAnimationFrame(animateFrame);
    }
}

表示速度改善の実践的アプローチ

1. 優先度に基づく最適化

まず、以下の順序で最適化を行うことをお勧めします:

  1. Critical Path の特定: 初期表示に必要な処理を明確にする
  2. Long Task の分割: 50ms以上の処理を分割する
  3. 不要な処理の削除: 使用されていない機能を削除する
  4. 遅延読み込みの実装: 必要に応じて段階的に読み込む

2. 継続的な監視と改善

Copy// パフォーマンス監視の実装
class PerformanceMonitor {
    constructor() {
        this.metrics = new Map();
        this.initializeObserver();
    }
    
    initializeObserver() {
        // Long Task の監視
        const observer = new PerformanceObserver((list) => {
            list.getEntries().forEach((entry) => {
                if (entry.duration > 50) {
                    console.warn('Long Task detected:', entry.duration);
                }
            });
        });
        observer.observe({ entryTypes: ['longtask'] });
    }
    
    measureFunction(name, fn) {
        const start = performance.now();
        const result = fn();
        const duration = performance.now() - start;
        
        this.metrics.set(name, duration);
        return result;
    }
}

最新技術とベストプラクティス

Service Worker の活用

Copy// service-worker.js
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open('v1').then((cache) => {
            return cache.addAll([
                '/app.js',
                '/styles.css',
                '/offline.html'
            ]);
        })
    );
});

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request).then((response) => {
            return response || fetch(event.request);
        })
    );
});

HTTP/2 Push の活用

Copy// 重要なリソースの先読み
const link = document.createElement('link');
link.rel = 'preload';
link.href = '/critical-script.js';
link.as = 'script';
document.head.appendChild(link);

ツールとフレームワーク活用法

Webpack での最適化

Copy// webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                },
            },
        },
    },
    plugins: [
        new webpack.optimize.ModuleConcatenationPlugin(),
    ],
};

TypeScript での型最適化

Copy// 型レベルでの最適化
interface User {
    id: number;
    name: string;
    email: string;
}

// 必要な部分のみを抽出
type UserSummary = Pick<User, 'id' | 'name'>;

// 計算量を削減
const userMap = new Map<number, UserSummary>();

実際の改善事例とlandinghubでの取り組み

私たちlandinghubでは、数多くのクライアント様のサイトでJavaScript実行時間の最適化を行ってきました。

事例1: ECサイトの商品検索最適化

課題: 大量の商品データの検索処理で画面が固まる 解決策:

  • 検索処理の非同期化
  • 結果の段階的表示
  • 仮想スクロールの実装

結果: 検索応答時間が70%短縮

事例2: 企業サイトのアニメーション最適化

課題: スクロールアニメーションでカクつきが発生 解決策:

  • requestAnimationFrame の使用
  • CSS Transform の活用
  • Intersection Observer による遅延実行

結果: 60fps の滑らかなアニメーションを実現

これらの成功事例を踏まえ、landinghubでは以下のようなアプローチで最適化を行っています:

  1. 詳細な現状分析: パフォーマンスツールを使った徹底的な調査
  2. 段階的な改善: 影響度の高い箇所から優先的に最適化
  3. 継続的な監視: 改善後のパフォーマンス維持

まとめ

JavaScript の実行時間最適化は、現代のウェブサイトにおいて不可欠な要素です。本記事で紹介したテクニックを実践することで、以下のような効果が期待できます:

  • ページ読み込み速度の向上
  • ユーザー体験の改善
  • SEO評価の向上
  • コンバージョン率の向上

重要なのは、一度に全てを実装しようとせず、段階的に改善を進めることです。まずは現状の測定から始めて、最も影響の大きい箇所から取り組んでみてください。

私たちlandinghubでは、このような表示速度の最適化を専門的にサポートしています。もしより本格的な最適化をお考えでしたら、ぜひお気軽にご相談ください。あなたのサイトに最適な改善策をご提案いたします。

パフォーマンスの最適化は継続的な取り組みが必要ですが、その効果は必ずユーザーに伝わります。今日から少しずつでも実践してみてくださいね。

関連記事

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です