Webフロントエンド ハイパフォーマンスチューニングを読んだよ 後編
この記事はは Webフロントエンド ハイパフォーマンスチューニングを読んだよ 前編 の記事の 続きになります。前回はフロントエンドのパフォーマンスチューニングを考える上で、基礎知識的なブラウザのレンダリングの仕組みや、目指すべきパフォーマンス指標についてまとめました。後編では、より実践的なJavaScriptやCSSを書く上でのチューニング方法についてまとめていきたいと思います。
Forced Synchronous Layout を減らす
通常、JavaScriptによるDOM操作はブラウザの再レンダリング処理を引き起こします。しかし、この再レンダリング処理によってパフォーマンスに大きく悪影響を与えるということはありません。しかし、Forced Synchronous Layout が引き起こされた場合は例外となり、パフォーマンスに悪影響を与えることになります。Forced Synchronous Layout とは、Forced Reflow とも呼ばれ、通常はスタイル評価のあとに行われるレイアウト算出が、最新のレイアウト情報が必要になったとき、再算出のために矯正実行されることを指します。
Forced Synchronous Layout が発生しないコードは以下になります。
const div = document.createElement('div');
div.innerHTML = 'ほげほげ';
document.body.appendChild('div');
以下のコードのように、追加したDOM要素のレイアウトを参照すると Forced Synchronous Layout が発生します。
const div = document.createElement('div');
div.innerHTML = 'ほげほげ';
document.body.appendChild('div');
console.log(div.offsetTop) // Forced Synchronous Layout 発生!!
このようにDOM操作などで、レイアウトに影響を与えるような操作を行ったあとに、レイアウトの情報を返すメソッドやプロパティを参照すると、レンダリングエンジンはDOM操作された結果の最新のレイアウト情報を返すために、その場でレイアウト計算を行ってしまいます。
レイアウトに影響を与えるような操作は以下のような操作です。
- style属性やstyleプロパティを通じて要素の大きさや座標に関わる操作を行う。
- DOM要素のclass属性を変更して要素の大きさや座標などを変更するスタイルを当てる
- DOMツリーの構造に変更を加える操作を行う(DOMの追加や削除など)
Forced Synchronous Layout をループ内で繰り返し起こしてしまうと、レイアウト計算処理が何度も繰り返されることで Layout Thrashing を引き起こします。
例えば、次のようなコードです。
const elements = document.querySelectorAll('.item');
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.style.marginTop = element.offsetHeight * 0.1;
}
Layout Thrashing は繰り返し Layout 処理を引き起こすため、JavaScriptの実行パフォーマンスを大幅に劣化させることがあります。 ループ処理の前に予めレイアウト情報を取得し、レイアウト情報の参照と更新を別のループ内で行うことで、Layout Thrashing を引き起こさずに同様の処理を実装できます。
例えば、以下のような実装です。
const elements = document.querySelectorAll('.item');
let heights = [];
for (let i = 0; i < elements.length; i++) {
heights[i] = elements[i].offsetHeight;
}
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.style.marginTop = heights[i] * 0.1;
}
ループ処理の前に予めレイアウト情報を取得し、レイアウト情報の参照と更新を別のループ内で行う実装方法はすでにdocument内にあるDOM要素の場合には有効でしたが、実装の都合によってはDOM操作する前にはレイアウト情報を取得できない場合があります。例えば、新たにDOMツリーに追加する要素のレイアウト情報を参照する場合です。
そういった場合は requestAnimationFrame()
メソッドを利用することで、Forced Synchronous Layout を回避することができます。なぜかというと、requestAnimationFrame()
メソッドに渡したコールバック関数の実行タイミングは、DOM操作が終わり、再レンダリングが終わってからだからです。そのため、以下のコードのように requestAnimationFrame()
のコールバック内でレイアウト情報を参照すれば、すでにレイアウト情報は再レンダリング処理の際に計算済みであるので、 Forced Synchronous Layout を回避できます。
const div = document.createElement('div');
div.textContent = 'ほげほげ';
document.body.appendChild(div);
requestAnimationFrame(() => {
console.log(div.offsetHeight);
})
レイアウトを避ける
前回の記事で紹介したように、Rendering の工程には Calculate Style と Layout という2つの工程が存在します。 Calculate Style はBEMなどを用いて1つのセレクタで記述するようにしたり、使用していないCSSを削除することで最適化することができますが、Layout を最適化するには何を行うと Layout が引き起こされるのかを知る必要があります。Layout を引き起こす原因を知った上でコードを記述することで、インタラクション実行時に Layout 自体を避けることができるようになります。
Layout を引き起こす典型的な原因は以下の3つになります。
- DOM要素の座標や大きさの変化
- DOMツリーの構造の変化
- DOM要素のコンテンツの変化
これ以外にも、ブラウザのウィンドウの大きさの変化でも Layout は引き起こされます。
上記の3つについてそれぞれ詳しく説明します。
DOM要素の座標や大きさの変化
CSSプロパティの中でも座標や大きさに関わるプロパティが変更された場合を指します。例えば、以下のようなプロパティがあります。
- height, width
- top, left, right, bottom
- margin, margin-top, marigin-left, margin-right, margin-bottom
- padding, padding-top, padding-left, padding-right, padding-bottom,
- border, border-width
CSSプロパティの変更はclass属性の変更や JavaScript によるCSSプロパティの変更のどちらでも同様に処理されます。
DOMツリーの構造の変化
appendChild()
や removeChild()
、 innerHTML
等によるDOMツリーの構造の変化でも Layout が引き起こされます。DOMツリーの構造の変化によって Calculate Style が引き起こされた後、 Layout が引き起こされます。
DOM要素のコンテンツの変化
テキストの文字数など、コンテンツ量が変化したときにも、 Layout が引き起こされます。
Layout の範囲を限定する。
全体のレンダリング処理が終わった後、JavaScript のDOM操作などによって、再度 Layout 処理が引き起こされた場合、 Layout の計算の対象となるのは多くの場合ドキュメント全体になります。DOM要素は子要素や親要素など複数のDOM要素に依存しているため、1部のDOM要素のレイアウトを変更する場合でも、結果としてドキュメント全体を再度 Layout する必要があるのです。しかし、DOM要素がある一定の条件を満たすことでそのDOM要素以下で Layout を更新する場合、 Layout の範囲をそのDOM要素以下に限定することができます。
ある一定の条件とは以下になります。
- svg要素である
type="text"
もしくはtype="search"
のinput
要素である- 次の条件を全て満たす要素である
- display プロパティが
inline
やinline-block
でない height
プロパティやwidth
プロパティがauto
以外に指定されている。さらにheight
プロパティは %値を使用していない。overflow
プロパティがscroll
かauto
かhidden
のいずれかであるtable
要素の子孫ではない
- display プロパティが
上記の条件のいずれかを満たすと、そのDOM要素に独自の Layout 範囲を生成します。
例えば、ある div
要素をレイアウト範囲として指定するには以下のようにCSSを記述します。
div {
display: block;
height: 200px;
width: 100px;
overflow: hidden;
}
ドキュメントのDOMツリーに含まれる要素がたくさんある場合、ドキュメントのレイアウトを再計算する場合にはすべての要素の Layout を再計算する必要があり、その分処理の時間が増えます。
レンダリングパフォーマンスを計測して、Layout 計算が多い場合には、CSSを調整して Layout範囲を限定することができます。
レンダリング結果の描画(Painting)のチューニング
ここでは、これまで計算した結果から実際のピクセルに変換する Painting の処理のチューニングについて書いていきたいと思います。
1度、Painting が終わり、完全にレンダリングが終わったとしてもインタラクションの度に Painting は再度引き起こされます。インタラクションに応じて Painting が再度引き起こされることを Repaint(再描画)と呼ばれます。
Paintingには内部に Paint Rasterize Composite Layers がありますが、これらの処理が再度引き起こされる原因は以下の3つになります。
- Rendering の Layout が引き起こされるとき
- Painting だけが引き起こされるとき
- Composite Layers のみが引き起こされるとき
どのCSSプロパティが何を引き起こすのかについては CSS Triggers というサイトで網羅的に紹介されています。こちらのサイトが参考になります。
まとめ
今回は Webフロントエンド ハイパフォーマンスチューニング という本を読んで、重要だと思った部分をまとめた記事の後編でした。この本には、今回紹介しきれなかった部分でとても有益な情報も多くありましたので、気になった方は本の方を購入して読んでみると面白いと思います。Amazonリンクはこちらになります。
また、自分も本を読んで得た知識を業務に活用して、よりパフォーマンスが良く、回覧者が見ていて気持ちの良いWebサイト制作に活かしていけたらと思います。