Back

JavaScriptのresizeイベントで陥りがちな落とし穴を回避する

JavaScriptのresizeイベントで陥りがちな落とし穴を回避する

windowのresizeイベントは一見単純に見えますが、アプリケーションが重くなり始めると話は別です。レスポンシブなJavaScriptコードがパフォーマンス問題を引き起こす理由について疑問に思ったことがあるなら、フロントエンドアプリケーションを悩ませる最も一般的なJavaScript resizeイベントの落とし穴の一つに遭遇している可能性があります。

この記事では、resizeイベントがパフォーマンスを悪化させる理由、スロットリングとデバウンシングによる最適化方法、そして最新のCSSソリューションとAPIを使用してJavaScriptを完全に回避するタイミングについて探ります。

重要なポイント

  • resizeイベントはウィンドウのリサイズ中に1秒間に数百回発火し、深刻なパフォーマンス問題を引き起こします
  • スロットリングとデバウンシングは、イベントハンドラーの実行頻度を制限するために不可欠な技術です
  • ResizeObserver APIやCSSコンテナクエリなどの最新の代替手段は、多くの場合より良いパフォーマンスを提供します
  • イベントリスナーの適切なクリーンアップは、本番アプリケーションでのメモリリークを防ぎます

JavaScript resizeイベントの隠れたパフォーマンスコスト

ユーザーがブラウザウィンドウをドラッグしてリサイズする際、resizeイベントは一度だけ発火するわけではありません。継続的に発火します。単純なウィンドウのドラッグ操作でも、イベントハンドラーを1秒間に数百回トリガーし、メインスレッドを関数呼び出しで溢れさせます。

// これは単一のリサイズ操作中に数百回ログを出力します
window.addEventListener('resize', () => {
  console.log(`Window size: ${window.innerWidth}x${window.innerHeight}`);
});

各イベント実行はメインスレッドをブロックし、ブラウザがレンダリング更新やユーザーインタラクションの処理などの他の重要なタスクを処理することを妨げます。結果は?ぎこちないアニメーション、応答しないインターフェース、そしてイライラしたユーザーです。

JavaScript resizeイベントの落とし穴がパフォーマンスにとって重要な理由

過度なイベント発火とメインスレッドのブロック

resizeイベントは、ウィンドウリサイズ中のピクセル変更ごとに発火します。ハンドラーが複雑な計算やDOM操作を実行する場合、本質的に高コストな操作を1秒間に数百回実行していることになります。

この一般的なパターンを考えてみましょう:

window.addEventListener('resize', () => {
  const elements = document.querySelectorAll('.responsive-element');
  elements.forEach(el => {
    // 各要素に対する複雑な計算
    el.style.width = calculateOptimalWidth(el);
  });
});

このコードは、リサイズ中に複数の要素を継続的に再計算・更新し、パフォーマンスのボトルネックを作り出します。

レイアウトスラッシング:静かなパフォーマンスキラー

最も陰湿なJavaScript resizeイベントの落とし穴は、要素の寸法を読み取って即座に新しいスタイルを書き込む場合に発生します。レイアウトスラッシングと呼ばれるこのパターンは、ブラウザに同期的にレイアウトを再計算することを強制します:

window.addEventListener('resize', () => {
  // レイアウト計算を強制
  const width = element.offsetWidth;
  
  // レイアウトを無効化
  element.style.width = (width * 0.8) + 'px';
  
  // 別のレイアウト計算を強制
  const height = element.offsetHeight;
});

各寸法読み取りは完全なレイアウト再計算をトリガーし、これが数百のresizeイベントによって倍増されます。

必須の最適化技術:スロットリングとデバウンシング

resizeイベント用のスロットリング実装

スロットリングは、resizeハンドラーの実行頻度を制限し、通常は60fps(16msごと)以下にします:

function throttle(func, delay) {
  let lastExecTime = 0;
  let timeoutId;
  
  return function (...args) {
    const currentTime = Date.now();
    
    if (currentTime - lastExecTime > delay) {
      func.apply(this, args);
      lastExecTime = currentTime;
    } else {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        func.apply(this, args);
        lastExecTime = Date.now();
      }, delay - (currentTime - lastExecTime));
    }
  };
}

const throttledResize = throttle(() => {
  // これは100msに最大1回実行されます
  updateLayout();
}, 100);

window.addEventListener('resize', throttledResize);

完全なリサイズ操作のためのデバウンシング

デバウンシングは、リサイズが停止するまで待ってから実行します:

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

const debouncedResize = debounce(() => {
  // リサイズ停止から250ms後に実行
  recalculateLayout();
}, 250);

window.addEventListener('resize', debouncedResize);

// クリーンアップを忘れずに
// window.removeEventListener('resize', debouncedResize);

windowリサイズイベントの最新の代替手段

CSSファーストソリューション:メディアクエリとコンテナクエリ

多くの場合、CSSを使用してJavaScript resizeイベントの落とし穴を完全に回避できます:

/* ウィンドウベースのレスポンシブ対応のためのメディアクエリ */
@media (max-width: 768px) {
  .sidebar { display: none; }
}

/* コンポーネントベースのレスポンシブ対応のためのコンテナクエリ */
.card-container {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card { grid-template-columns: 1fr 1fr; }
}

JavaScriptベースのメディアクエリ検出には、matchMediaを使用します:

const mediaQuery = window.matchMedia('(max-width: 768px)');
mediaQuery.addEventListener('change', (e) => {
  // ブレークポイントを越えた時のみ発火
  if (e.matches) {
    showMobileMenu();
  }
});

ResizeObserver API:パフォーマンスに優しい代替手段

ResizeObserverは、パフォーマンスペナルティなしで要素固有のサイズ監視を提供します:

const resizeObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    // ブラウザがサイズを提供—強制リフローなし
    const { width, height } = entry.contentRect;
    updateElementLayout(entry.target, width, height);
  }
});

resizeObserver.observe(document.querySelector('.responsive-container'));

// 完了時のクリーンアップ
// resizeObserver.disconnect();

本番環境対応のリサイズハンドリングのベストプラクティス

  1. メモリリークを防ぐため、常にイベントリスナーをクリーンアップする
  2. 適切なツールを選択する:スタイリングにはCSS、要素監視にはResizeObserver、必要な場合のみスロットル化されたresizeイベントを使用
  3. Chrome DevToolsのPerformanceパネルを使用してパフォーマンスへの影響を測定する
  4. 該当する場合、より良いスクロールパフォーマンスのためにパッシブリスナーを検討する

まとめ

これらのJavaScript resizeイベントの落とし穴を理解し、適切なソリューションを実装することで、すべてのデバイスでスムーズに動作するレスポンシブインターフェースを構築できます。重要なのは、特定のユースケースに適したアプローチを選択することです。CSSベースのソリューション、最新のAPI、または適切に最適化されたイベントハンドラーのいずれであっても。可能な限りCSSから始め、要素固有の監視にはResizeObserverを使用し、ウィンドウレベルの監視が本当に必要な場合にのみスロットル化またはデバウンス化されたresizeイベントを予約してください。

よくある質問

スロットリングは継続的なリサイズ中に固定間隔で実行を制限し、ユーザーがドラッグしている間定期的に実行します。デバウンシングはリサイズが完全に停止するまで待ってから一度実行します。リアルタイム更新にはスロットリングを、最終計算にはデバウンシングを使用してください。

ResizeObserverは、Chrome、Firefox、Safari、Edgeを含む最新ブラウザで優れたサポートを持っています。古いブラウザでは、ポリフィルを使用するか、機能検出でスロットル化されたresizeイベントにフォールバックして互換性を確保してください。

スタイリングがビューポートではなく要素のサイズに依存する場合にコンテナクエリを使用してください。JavaScriptのオーバーヘッドやパフォーマンスの懸念なしに、コンポーネントベースのデザイン、カードレイアウト、レスポンシブタイポグラフィに最適です。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay