はじめに (対象読者・この記事でわかること)
この記事は、JavaScriptの基礎的な知識がある方、特にDOM操作に慣れてきた開発者を対象にしています。WebサイトやWebアプリケーションを開発する上で、要素のスタイルを変更した後にその変更が画面に反映されるのを待ってから次の処理を実行したい場面に遭遇したことがある方に特に役立つ内容です。
この記事を読むことで、ブラウザのレンダリングプロセスを理解し、スタイル変更が反映されるタイミングを正確に制御する方法がわかります。requestAnimationFrameやsetTimeoutを使った具体的なコード例を通して、パフォーマンスを損なわずに正確なタイミングで処理を実行するテクニックを習得できます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - JavaScriptの基本的な文法と概念 - DOM操作の基本的な方法(getElementById、querySelectorなど) - 非同期処理の基本的な理解(Promiseやasync/関数)
ブラウザのレンダリングプロセスとスタイル変更
Webブラウザは、HTML、CSS、JavaScriptを組み合わせてユーザーに表示されるページを生成します。このプロセスは複数のステップで構成されており、特にJavaScriptによるスタイル変更は、レンダリングパイプラインの特定の段階で処理されます。
ブラウザは通常、以下のようなステップでページをレンダリングします。 1. DOMツリーの構築 2. CSSOM(CSSオブジェクトモデル)の構築 3. レンダーツリーの構築 4. レイアウト(リフロー) 5. ペイント(リペイント)
JavaScriptで要素のスタイルを変更すると、通常はステップ4のレイアウトとステップ5のペイントがトリガーされます。しかし、これらの処理は即座に行われるわけではなく、ブラウザが最適なタイミングでまとめて処理する場合があります。
このため、JavaScriptでスタイルを変更した直後にその変更が画面に反映されているとは限りません。特に、アニメーションや遷移効果を実装する際には、スタイル変更が反映されるのを待ってから次の処理を実行する必要があります。
スタイル変更が反映されるのを待つ具体的な方法
JavaScriptでスタイル変更が反映されるのを待つ方法はいくつかあります。それぞれの方法の特徴と使い分けについて解説します。
requestAnimationFrameを使う方法
requestAnimationFrameは、ブラウザが次の描画(リペイント)の準備ができたときに指定されたコールバック関数を実行するためのAPIです。アニメーションや視覚的な変更を扱う際に最も適した方法です。
Javascriptfunction changeAndWait() { const element = document.getElementById('myElement'); // スタイルを変更 element.style.width = '200px'; element.style.height = '200px'; // 次の描画サイクルで処理を実行 requestAnimationFrame(() => { console.log('スタイル変更が反映されました'); // ここにスタイル変更後に実行したい処理を記述 }); } changeAndWait();
この方法の利点は、ブラウザの描画タイミングと完全に同期できることです。また、ページが非表示の状態(タブがアクティブでないなど)では実行されないため、リソースの無駄遣いを防げます。
setTimeoutを使う方法
setTimeoutを使って、短い遅延を設けてから処理を実行する方法もよく使われます。requestAnimationFrameが利用できない場合の代替手段として有効です。
Javascriptfunction changeAndWait() { const element = document.getElementById('myElement'); // スタイルを変更 element.style.width = '200px'; element.style.height = '200px'; // 短い遅延を設けてから処理を実行 setTimeout(() => { console.log('スタイル変更が反映されました'); // ここにスタイル変更後に実行したい処理を記述 }, 0); } changeAndWait();
setTimeoutの第二引数に0を指定すると、現在の実行コンテキストが終了した直後にコールバック関数が実行されるようスケジュールされます。これにより、ブラウザがレンダリング処理を行う機会を得られます。
ただし、この方法はrequestAnimationFrameほど正確ではない場合があります。特に複雑なページや低速な環境では、タイミングがずれる可能性があります。
Promiseと組み合わせた方法
より洗練されたアプローチとして、Promiseと組み合わせてスタイル変更が反映されるのを待つ方法があります。これにより、非同期処理として扱えるため、async/await構文と組み合わせやすくなります。
Javascriptfunction waitForNextFrame() { return new Promise(resolve => { requestAnimationFrame(resolve); }); } async function changeAndWait() { const element = document.getElementById('myElement'); // スタイルを変更 element.style.width = '200px'; element.style.height = '200px'; // 次のフレームを待つ await waitForNextFrame(); console.log('スタイル変更が反映されました'); // ここにスタイル変更後に実行したい処理を記述 } changeAndWait();
この方法は、複数のスタイル変更を順番に適用する場合や、他の非同期処理と組み合わせる場合に特に便利です。
複数のスタイル変更を一度に行う場合
要素の複数のプロパティを変更する場合、個別に変更するのではなく、CSSクラスを変更する方がパフォーマンスが良い場合があります。この場合も、変更が反映されるのを待つ必要があります。
Javascriptfunction changeClassAndWait() { const element = document.getElementById('myElement'); // CSSクラスを変更 element.classList.add('new-style'); // クラス変更が反映されるのを待つ requestAnimationFrame(() => { console.log('クラス変更が反映されました'); // ここにクラス変更後に実行したい処理を記述 }); } changeClassAndWait();
ハマった点やエラー解決
スタイル変更が反映されないように見える問題
実際に開発を行うと、スタイル変更が反映されないように見える問題に遭遇することがあります。特に、要素の表示/非表示を切り替える際や、アニメーションを実装する場合に発生しやすい問題です。
Javascript// 問題のあるコード function toggleElement() { const element = document.getElementById('myElement'); element.style.display = 'none'; // 非表示にする console.log('要素は非表示です'); element.style.display = 'block'; // 再表示する console.log('要素は再表示されました'); }
このコードでは、要素が一瞬非表示になるだけで、実際には再表示されていることがわかりにくい場合があります。
解決策
この問題を解決するには、スタイル変更をブラウザのレンダリングタイミングと同期させる必要があります。requestAnimationFrameを使うことで、この問題を回避できます。
Javascript// 解決策 function toggleElement() { const element = document.getElementById('myElement'); element.style.display = 'none'; // 非表示にする // 次のフレームで再表示する requestAnimationFrame(() => { element.style.display = 'block'; // 再表示する console.log('要素は再表示されました'); }); } toggleElement();
このようにすることで、ブラウザが一度レンダリング処理を行った後に要素を再表示するため、変更が正しく視認できるようになります。
複数のスタイル変更がまとめて反映される問題
複数のスタイル変更を連続で行う場合、ブラウザがそれらをまとめて処理してしまうことがあります。これにより、各変更の間に処理を挿入したい場合に問題が発生します。
Javascript// 問題のあるコード function animateElement() { const element = document.getElementById('myElement'); // 複数のスタイル変更を連続で行う element.style.width = '100px'; element.style.height = '100px'; element.style.backgroundColor = 'red'; // これらの変更がすべて反映される前に実行されてしまう console.log('アニメーション開始'); }
解決策
この問題を解決するには、各スタイル変更の後にrequestAnimationFrameを挟むことで、ブラウザにレンダリングの機会を与えます。
Javascript// 解決策 function animateElement() { const element = document.getElementById('myElement'); // 各スタイル変更の後にフレームを待つ element.style.width = '100px'; requestAnimationFrame(() => { element.style.height = '100px'; requestAnimationFrame(() => { element.style.backgroundColor = 'red'; console.log('アニメーション開始'); }); }); } animateElement();
この方法では、各スタイル変更が反映されるたびに処理が実行されるため、より細かい制御が可能になります。ただし、コールバックがネストされてしまうため、async/awaitを使った方が可読性が高くなります。
Javascript// async/awaitを使った解決策 function waitForNextFrame() { return new Promise(resolve => { requestAnimationFrame(resolve); }); } async function animateElement() { const element = document.getElementById('myElement'); // 各スタイル変更の後にフレームを待つ element.style.width = '100px'; await waitForNextFrame(); element.style.height = '100px'; await waitForNextFrame(); element.style.backgroundColor = 'red'; console.log('アニメーション開始'); } animateElement();
まとめ
本記事では、JavaScriptでDOMのスタイル変更が画面に反映されるのを待つ方法について解説しました。
- requestAnimationFrameを使う方法が最も正確でパフォーマンスに優れています
- setTimeoutを使う方法は簡単ですが、精度が低い場合があります
- Promiseと組み合わせる方法では、async/awaitと組み合わせてより読みやすいコードが書けます
- 複数のスタイル変更を扱う場合は、変更を適切に区切ってレンダリングタイミングと同期させる必要があります
この記事を通して、ブラウザのレンダリングプロセスを理解し、JavaScriptによる視覚的な変更を正確に制御する技術を習得できたことと思います。これにより、より滑らかでパフォーマンスの高いWebページやWebアプリケーションを開発できるようになります。
今後は、CSS TransitionsやAnimationsとの連携や、パフォーマンスをさらに最適化するための高度なテクニックについても記事にする予定です。
参考資料
- MDN Web Docs - requestAnimationFrame
- MDN Web Docs - setTimeout
- CSS Rendering Performance - Google Developers
- High Performance Animations - CSS-Tricks