はじめに
縦長のLPを実装中に、ちょっと凝ったサイズの大きい背景画像を複数配置していたときに遭遇した現象についてまとめます。
前提として、今回のJSは「ページの最終的な高さ」に応じてCSSプロパティを動的に設定するという、レイアウト関連の処理です。
パフォーマンスのためにJSの実行タイミングを最適化したつもりでいましたが、基礎的なイベントの原則を忘れるとレイアウトが崩れる原因になるということを改めて知ったので、共有します。
発生した現象:スーパーリロードでしか起きない😱
シークレットモードでページ確認時に発覚したのですが、いわゆる「スーパーリロード(Win: Ctrl + F5、Mac: Command + Shift + R)」でのみ、ページ下部に大きな余白が生まれるレイアウト崩れが発生しました。
これは通常リロード(キャッシュが有効)では再現せず、原因特定が難航しました。
キャッシュを無視するスーパーリロード時、初めてリソースをゼロから読み込むときにのみ発生したことがヒントとなりました。
原因は、何のことはない「JSの実行タイミング」でした…。
問題の再現と根本原因
問題を発生させていたのは、ページの最終的な高さに依存するレイアウト計算でした。
サンプルコード
実際に問題を発生させていたコードの初期化処理(要点抜粋)は以下の通りです。
document.addEventListener('DOMContentLoaded', () => {
// このタイミングでは、まだ大きな画像などのリソースがロードされていない可能性が高い
const defaultBodyHeight = document.body.scrollHeight;
const container = document.querySelector('.exampleContainer');
// 取得した高さに基づいてコンテナのサイズを計算・設定
container.style.height = `${defaultBodyHeight - 10000}px`;
// ... その他の処理 ...
});
根本原因:DOMContentLoadedの原則
DOMContentLoadedイベントは、HTMLの解析が完了した時点で発火しますが、画像や外部CSSなどのリソースの読み込み完了は待機しません。
- スーパーリロード時、
DOMContentLoadedの時点でまだ巨大な画像リソースが未ロードのため、document.body.scrollHeightが 「本来より低い」誤った値を返す - この誤った高さに基づいてレイアウト計算が行われてしまう
- 直後に画像がロード完了し、ページが 「正しい最終的な高さ」 に変わったとき、計算ミスによる余白(レイアウト崩れ)が残ってしまう
解決方法
window.onloadで「全てのリソースの読み込みが完了してから」実行するように変更しました。
サンプルコード
window.addEventListener('load', () => {
// 全てのリソースがロード完了後なので、正確な高さを取得できる
const defaultBodyHeight = document.body.scrollHeight;
const container = document.querySelector('.exampleContainer');
// 取得した(正確な)高さに基づいてコンテナのサイズを計算・設定
container.style.height = `${defaultBodyHeight - 10000}px`;
// ... その他の処理 ...
});
実行時にdocument.body.scrollHeightがページの最終的な正しい値を返すようになり、レイアウト崩れが解消しました。
まとめと考察
この問題が解決した今だからこそ言えるのですが、改めてWeb開発の基本原則を確認しましょう。(自戒の念も込めて)
ほとんどのケースで、パフォーマンスの観点からDOMContentLoadedを使うのがベストプラクティスです。
DOMContentLoadedが適している処理
例えば、ボタンクリック時のイベントリスナー設置、アコーディオンやタブメニューの開閉処理、入力フォームのバリデーション初期設定など、要素のサイズや位置の正確な確定を待たない処理などは、DOMContentLoadedが適しています。
window.onloadが適している処理
今回のように、「ページの正確な最終寸法(高さ・幅)を取得する必要がある」場合など、リソースの読み込みが完了してから実行したい場合、window.onloadが適しています。
補足
今回はシンプルさと確実性を優先し、すべてのリソースの読み込み完了を待つwindow.onloadを採用しました。
しかし、パフォーマンスを求める場合、window.onloadは遅延の原因となる小さなリソースの読み込みも待ってしまうというデメリットがあります。
このようなケースではDOMContentLoadedを使いつつ、レイアウトに影響を与える特定の大きな画像だけを個別に選んで、そのロードが全て完了したことを監視し、処理を実行するというような手法もあります(Promise.allなど)。
複雑にはなりますが、この個別監視の手法は、window.onloadよりも早く処理を開始できる可能性があります。
これらの使い分けを意識することで、パフォーマンスを犠牲にすることなく、動的なレイアウトの正確性を確保することができます。
安定したWeb開発の土台として、自身のプロジェクトでイベントの使い分けが正しく行えているか、改めて確認するきっかけとなれば幸いです。