HTMLやCSSのパフォーマンスチューニングを皆さん普段意識されているでしょうか?
フロントエンドはJavaScriptの進化と共にアプリケーションが年々複雑化してきたこともあり、パフォーマンスチューニングのトピックを目にする機会が増えました。
しかしHTMLやCSSも同様にレンダリングの仕組みを少し知り、それに応じた実装することでパフォーマンスを大きく上げることも可能です。
今回は最もシンプルかつ分かりやすい事例として、will-change
プロパティについて紹介したいと思います。
サンプルを使ってFPSを実際に測定してみる
今回はサンプルとして、TwitterのツイートにあたるHTML構造と同じものを4000個縦に並べ、高さ400pxでスクロールできる領域を用意し、スクロール時のFPS(Frame Per Second)を測定してみました。
// この実装は実質たった3行!
const createListItems = (numberOfItems) =>
[...Array(numberOfItems)].map(() => `ツイートのHTML`).join('');
document.getElementById('scrollable-list').innerHTML = createListItems(4000);
こちらがwill-change属性を付与しなかった時の様子(※右上のfpsに注目!)
こちらがwill-change属性を付けた時の様子
右上の「fps」のところを見て頂ければ分かるように、前者は1桁台のfpsを記録するのが多いのに対して、後者は10後半~20前半くらいで安定しています。
ちなみに両者の違いはたった一行、以下のCSSを加えただけです。
will-change: transform;
「will-changeなんて見たこともなかった」なんて人も多いかもしれません。
なぜこれほどまでの差が出るのでしょうか。
will-transformの指定によって新しいコンポジット層が作られる
厳密にはブラウザによって走る処理の内容は異なるのですが、概ねの共通処理としては、transformやscrollOffsetが変更された時に、新たに「コンポジットレイヤー」なるものを生成してくれます。
するとスクロール時にリペイントが走る領域が限定され、例えばoverflow: auto
を指定した領域ではスクロールする度に全要素が描画されますが、コンポジットレイヤーが生成されていると逐次全描画されなくなります。
実際にリペイントの様子を見てみましょう。Google Chromeの機能を使って、再描画される要素は緑色に表示するようにしました。
こちらがwill-change属性を付与しなかった時の様子
こちらがwill-change属性を付けた時の様子
ご覧のようにwill-change属性を付与するとリペイントされる領域が極めて小さくなることが分かると思います。
ちなみにこれはoverflow: autoを指定して、任意の領域にスクロールを設けた時に発生する現象で、デフォルトのbody要素をスクロールするときには今回のケースは当てはまりません。
transformのハックを使う必要はない
スマホが急激に普及し始めた頃、ウェブアプリケーション開発では当時今よりもずっと低い性能だったモバイルでいかに滑らかなアニメーションを実現するのかが課題となっていました。
その時GPUの力を活かすべく「GPUアクセラレーションを有効にする」という方法が重宝され、以下のようにCSSハックが行われていました。
transform: translate3d(0, 0, 0);
これを指定するとwill-changeと同じくコンポジットレイヤーが生成されるので、GPUアクセラレーションが有効になると共に、リペイントの領域も限定できるので、上記CSSを本来の用途とは異なる形で使われることが多かったのです。
しかし実際にtransformを指定するのと、will-changeを指定するのとでは意味合いが異なります。
will-change
属性は名前の通り、これから発生する変更をブラウザに伝えるもので、ブラウザは時間的猶予を持って、あらかじめコンポジットレイヤーを生成することができます。
もしもwill-changeで知らされていないと、アニメーションが始まる時にコンポジットレイヤーを生成しなければならないので、その時に負担がかかり、アニメーション実行前に少しのタイムラグが発生してしまうのです。
後にモダンブラウザには軒並みwill-changeが実装されたことで、このハックが使用されることは少なくなり、今から開発する分にはほとんど不要になったと言っても良いでしょう。
(※追記: IEとEdgeには対応してなかったようです。PC向けサイトを作る際は依然として必要とのことで訂正させて頂きます。)
ちなみにコンポジットレイヤーの生成や保持も負荷につながるので、理想を言えばアニメーション前に生成し、終わったあとには属性をJavaScriptで消すのが最もパフォーマンスにとって理想的です。(現実的にそこまでやっている事例は少ないと思いますが)
リペイントがどんな状況で発生するのかを知っておく
今回は分かりやすい例としてwill-changeを例にしましたが、削減可能なリペイントが起きるのは独自のスクロール領域を設けたときだけではありません。
他にはposition :fixed
を指定すると、body要素をスクロールした時にも常にリペイントが走りますが、will-change属性を指定するとリペイントを防ぐことができます。
もしも固定ヘッダーに複雑な構造が存在していれば、will-changeを設定するだけでいくらかのパフォーマンス改善になるでしょう。
また話を広げると、どのような場面でリペイント、ひいてはリフローが発生するのかを知っておくことがHTMLやCSSのパフォーマンス改善にとって重要なトピックになります。
マークアップはパフォーマンスという観点が抜けやすく、実装者の知識も問われることが少ないですが、今回事例として示したようにちょっとした改善でパフォーマンスが向上する可能性があるということを理解して頂ければと思います。