JavaScriptによるページの応答遅延検出と対処法

現代のWeb開発では、ユーザー体験の質がサイトの成功を左右します。スムーズに動作するページはユーザーの満足度を高め、滞在時間を延ばしますが、応答が遅れるページは離脱の原因となります。ここでは、JavaScriptを用いてページの処理遅延(いわゆる「カクつき」や「もたつき」)を検出し、改善する手法を解説します。

処理遅延の本質

ページの応答が遅れる主な原因は、JavaScriptのメインスレッドが長時間占有されることです。重い計算処理、過剰なDOM操作、メモリリーク、または非効率なリソース読み込みが主な要因です。これらを的確に把握するには、実行時間の監視が不可欠です。

検出方法

メインスレッドのブロッキング状況を計測するには、以下の2つの手法が一般的です。

1. setTimeoutとperformance.now()を利用した簡易計測

設定した遅延時間と実際のコールバック実行時間の差を測定します。100msのタイマーを仕掛け、その実行が予定より遅れた場合に遅延を検出します。

function monitorLag() {
  const scheduled = performance.now();
  setTimeout(() => {
    const actual = performance.now();
    const diff = actual - scheduled;
    if (diff > 100) {
      console.warn(`遅延検出: 想定100msに対し${diff.toFixed(1)}ms`);
    }
  }, 100);
}

// 定期的な監視を開始
setInterval(monitorLag, 1000);

2. requestAnimationFrameによるフレームレート監視

ブラウザの描画サイクルに同期して計測するため、より正確な遅延検出が可能です。直近60フレームの平均持続時間を算出し、16.7ms(60fps相当)を超えた場合に警告を出します。

const frameDurations = [];

function watchFrameTiming() {
  let previous = performance.now();

  function measure() {
    const now = performance.now();
    const elapsed = now - previous;
    frameDurations.push(elapsed);

    // 最新60フレームのみ保持
    if (frameDurations.length > 60) {
      frameDurations.shift();
    }

    const avg = frameDurations.reduce((a, b) => a + b, 0) / frameDurations.length;
    if (avg > 16.7) {
      console.warn(`フレームレート低下: 平均 ${avg.toFixed(1)}ms`);
    }

    previous = now;
    requestAnimationFrame(measure);
  }

  requestAnimationFrame(measure);
}

watchFrameTiming();

改善策

遅延を検出した後の具体的な対処法を以下に示します。

コードの最適化

ループ処理やDOM操作を効率化します。例えば、forEachよりforループの方がオーバーヘッドが少ない傾向があります。また、複数のスタイル変更は個別に行うより、cssTextで一括適用した方が高速です。

// 非効率な例
items.forEach(item => {
  const el = document.getElementById(item.id);
  el.style.width = item.w + 'px';
  el.style.height = item.h + 'px';
});

// 改善例
for (let i = 0; i < items.length; i++) {
  const el = document.getElementById(items[i].id);
  el.style.cssText = `width: ${items[i].w}px; height: ${items[i].h}px;`;
}

リソースの遅延読み込み

Intersection Observerを用いれば、画面に表示されるタイミングまで画像の読み込みを遅らせられます。

const lazyImgs = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});
lazyImgs.forEach(img => observer.observe(img));

Web Workersによる並列処理

計算負荷の高い処理をメインスレッドから分離します。

メインスレッド側

const worker = new Worker('calculate.js');
worker.postMessage({ task: 'heavy', limit: 1e9 });
worker.onmessage = (e) => {
  console.log('計算結果:', e.data);
};

calculate.js

self.onmessage = function (e) {
  if (e.data.task === 'heavy') {
    let total = 0;
    for (let i = 0; i < e.data.limit; i++) {
      total += i;
    }
    self.postMessage(total);
  }
};

メモリ管理の徹底

不要になったオブジェクトは積極的にnullを代入し、WeakMapやWeakSetを活用してガベージコレクションを促進します。

const cache = new WeakMap();
let user = { name: 'Taro' };
cache.set(user, { lastAccess: Date.now() });
// userが不要になったら
user = null;  // キャッシュ内の参照も自動解放される

これらの手法を組み合わせることで、レスポンスの良い快適なWebページを実現できます。定期的なパフォーマンス計測とコードの見直しが、長期的な品質維持に役立ちます。

タグ: javascript パフォーマンス最適化 requestAnimationFrame Web Workers Intersection Observer

6月19日 19:03 投稿