サイトパフォーマンスを上げるJavaScriptとCSSの使い方

https://engineering.gosquared.com/optimising-60fps-everywhere-in-javascript

1 comment | 2 points | by WazanovaNews 3年弱前 edited


Jshiike 3年弱前 edited | ▲upvoteする | link

リアルタイムアナリティクスのサービスを提供しているGoSquaredがエンジニアブログで紹介しているのは、サイトパフォーマンス向上の工夫。今回は、アセットのダウンロードやパースのところでなく、遅延をおこさずにスムーズに描画するかというポイントに絞っています。

典型的なスクリーンの描画フローでは、フレームごとにブラウザがJavaScriptを評価する。もしJavaScriptによって修正されていれば、エレメントのためのスタイルやレイアウトを再計算する。次に、ページをいくつかのレイヤに描いていき、レイヤをスクリーンにあてはめるのにGPUを使う。各ステージごとに、ウェブページやアプリが行うことが違い、それぞれにコストがかかる。スムーズな60fpsを目指したければ、ブラウザは全てを16msで完了させる必要がある。

JavaScriptがレイアウトを変更(margin, padding, width, height等)した場合、ブラウザはすぐに再計算するわけではない。再計算が必要な箇所をトラックし、次の読込みのタイミング(offsetWidthなどのプロパティにアクセスしJavaScriptによって、もしくはスクリーンにページを描画するタイミングがきたことでレンダラーによって)まで再計算を先延ばしにする。よって、フレームごとにレイアウトを再計算させるよりも、変更のキューをなるべく積み上げる方がよい。

1) 再レイアウト

再計算を余計に繰り返す事例: ループ毎にブラウザがsomeOtherElement.offsetWidthを再計算し、次のwidthをアップデート。そしてsomeOtherElementのoffsetWidthプロパティが無効になるので、次の繰り返しの際にこの値も再計算するというもっとコストのかかる作業になる。

// els is an array of elements
for(var i = 0; i < els.length; i += 1){
  var w = someOtherElement.offsetWidth / 3;
  els[i].style.width = w + 'px';
}

計測結果

widthの変更がsomeOtherElementのサイズに影響を与えない場合: 今回は、まずプロパティを読込み、それからスタイルの更新を反映させる。ブラウザは、someOtherElement.offsetWidthを読込むためのリフロー(エレメントの大きさ、位置の再計算)は1回で済む。そして、elsのエレメントの更新はキューに溜まり、次のタイミング(後に続くJavaScript、もしくはエレメントのリペイント)でまとめて適用される。

var x = someOtherElement.offsetWidth / 3;
for(var i = 0; i < els.length; i += 1){
  els[i].style.width = x + 'px';
}

計測結果

リフローと再レイアウトは最小限に。プロパティは全て最初に読込み、更新はその後まとめて適用すること。

2) リペイント

リペイントはフレームごとに発生する。ブラウザはキューに積み上がって再計算された結果のピクセルを再描画する。スムーズにアニメションするには、リペイントがなるべく効率的であること。ブラウザにとってコストの高いbox shadowやgradientを避ける。それらのプロパティを保持しているエレメントのアニメーションをしない、もしくは、それらのエフェクトでリペイントを起こすことになるプロパティをアニメーションしないというのも重要である。

ブラウザは通常、全ての変更するピクセスを含む最小の長方形を描くことで、異なるエリアを一つのリペイントでまとめて効率的に描こうとする。ページの異なる複数のエリアのエレメントを変更した場合が特にまずい。例えば、ウェブアプリがスクリーンの反対側にある両角を修正し、ページの全てがその変更ポイントを囲む長方形に含まれた場合。Atomエディタで対処した事例はこちらのブログにある。ベストな対策は、エレメントが異なるレイヤでレンダリングされるようにすること。

3) レイヤ、コンポジット、CPUとGPU

かつては、ブラウザは一つのフレームをメモリに保持し、CPUがピクセルを直接フレームに書込むことでスクリーンを描いていた。最近のブラウザは、CPUでエレメントをレイヤに描き、GPUでレイヤをまとめ、スクリーンに最終的なピクセルを表示させる。

GPUは、2D/3Dスペースで複数のレイヤを動かしたり、レイヤをローテーションとかスケールさせたり、異なる透明度で表現したり、というベーシックな描画のオペレーションをとても効率的にこなす。その手のプロパティを使ってエレメントをアニメーションしているのであれば、その利点を使える。

以下の二つの事例は、わざと効果が際立つように工夫してみた。かなりヘビーなbox shadowを使っている100の を用意し、CSS transitionを使って水平にアニメーションしている。

left プロパティを使った事例: ブラウザは各フレームで関連するエレメントのピクセルを再計算しなくてはいけないので、相当は計算コストがかかる。

JS Binのview

Chrome Dev Toolでのタイムライン

同じエレメントをtransformでアニメーションした事例: ここでは、スクリーンにまとめて表示される前に、transformがブラウザに強制して、各

エレメントがGPUでそれぞれのレイヤになっている。よって各フレームにおいての残り作業は、レイヤごとの新しい位置を計算するだけなので、コンピュータの能力はほとんどくわない。box shadow や background gradient の再計算はない。レイヤ内でのピクセルの変更がないので、タイムラインには "Paint" はなく、"Composite Layers" が表示されているだけである。

JS Binのview

Chrome Dev Toolでのタイムライン

ブラウザにエレメントをレイヤに置くようにさせるにはいくつかの方法がある。通常は、CSS transitionをtransformやopacityで適用することで十分。transform: translateZ(0) を使うというハックもある。実際のビジュアル効果はないが、ブラウザは3D transitionがあると把握するので、エレメントを新しいレイヤにもっていくことができる。但し、新しいレイヤで上書きできてしまうので要注意。Appleのホームページはそのせいで、コンポジットするレイヤが多すぎてサイトが遅くなっている

Chrome DevToolで、“show paint rectangles” と “show composited layer borders” を有効にして確認してみること。

モバイルでスムーズな動きを実現したければ、GPUアクセルレーションを利用する transformと opacity のプロパティだけをアニメーションすること。モバイルのプロセッサーはGPUと比較するとひどいので、widthやheightなどのプロパティは避けるべき。例えば、サイズを変更するのと同じ効果は、エレメントのtransformを別のエレメントにoverflowでアニメーションすることで実現できる。

GoSquaredのUIのモーダルビューでの事例

JS Binのview

  • 大きめの radial gradientであるオーバーレイは、独自のGPUレイヤを持つために、transform: translateZ(0) を使っている。フルスクリーンのオーバーレイであるため、残りのユーザインターフェースと同じレイヤでレンダリングすると、全体がリペイントされてしまう。独自のレイヤをもち、opacityプロパティのみをアニメーションすることで、GPUで完結するゆえに、とても効率的。
  • モーダルビューそのものは、独自のレイヤをもち、translate3dを使ったtranslateプロパティを利用してアニメーションされている。これは、モーダルにbox shadowを適用することもできるということ。中に何を入れるかはあまり気にしなくてもよい。いずれにしても全体のアニメーションがリペイントなしでGPUがハンドルしてくれるからだ。

#サイトパフォーマンス  #javascript #css

Back