するするさせたい:サイボウズ採用情報のアニメーション(その2)

サイボウズの採用情報ページにあるアニメーションがするするしていない。ので前回は何がおこってるのか見てみた。

今回はするするさせられないか、がんばってみる。

  • その1 ― 何が起こっているのか調べる
  • その2 ― 調べながら直しながらするするさせる
  • その3 ― ちょっとしたことや他のブラウザでもするするさせる

アニメーションを止める

まず、いまのjQueryベースなアニメーションを止める。
コンソールに以下をぶっこめばアニメーションが止まって、背景も初期位置に戻る。

jQuery('.icon').stop().css('backgroundPosition', '')

チェーンもできるしjQueryべんりだね。

CSSアニメーションにしてみる

jQueryのアニメーションを別の方法に書き換えるわけだけど、今は2010年代も後半だ。使うならCSSアニメーションしかない。とくに右から左へ一方向っていう単純な動きならなおさらだ。

さっきのjQueryのコードは、こう変換できるかな。

@keyframes loop-icon {
  from { background-position: 0 }
  to { background-position: -1680px }
}
.icon {
  animation-name: loop-icon;
  animation-duration: 60480ms; /* 1680×36 */
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  /* animation: loop-icon 60480ms linear infinite; でもOK */
}

で、これを document.head.innerHTML += <style>...</style> とかでくるんでコンソールにぶっこむとアイコンが動き出す。おー、なんとなくするする動いてる感じ。

描画の詳細を見てみる

なんとなくじゃだめなので、またタイムラインを録ってみる。

f:id:myakura:20160513131612p:plain

およ…?

CPUの使用量が下がった感じはするけど、引き続きジャンクな感じ。
拡大すると、たしかに下がってはいる。

f:id:myakura:20160513131746p:plain

あと、Summaryから“Scripting”の項目が消えている。これはjQueryのアニメーションが setInterval() ベースだったんだけど、それがなくなったからだ。

さて、多少の成果が出たけれど、これではするするとは言えない。もっと調べよう。
拡大して、太い緑色のところをクリックすると、その描画についての詳細を見られる。

f:id:myakura:20160513132737p:plain

描画範囲(Dimensions)が「1362 × 6935」となっている。なんと、アニメーションはページの一部しかないのに、どうやらページ全体を描画しているらしい。だから負荷が高いまま。

will-change で別レイヤーに移す

ページの一部(高さ120px)を動かすためだけにページ全体(6935px)が毎回描画されるのはだいぶつらい。再描画の範囲を最小にしたい。そこで注目するのが、さっきのSummaryにあった「Layer Root」という項目だ。

ブラウザの描画領域は一枚なときもあるけど、合成レイヤー(compositing layer)という複数のレイヤーからなっていることもある。今回のように一部だけ動くとか、そういう際にブラウザが自動的にレイヤーを切って管理してくれる。OHPシート重ねて動かすみたいなやつだ。

というわけで、ブラウザに「動くのここだけだからレイヤー切ってよー」というのを示して全体を描画させないようにする。これには will-change なるプロパティを使う。値にプロパティ名を指定すると、ブラウザはその要素上のそのプロパティが変化するのに適した処理をしてくれるようになる。

レイヤーを新たに作りたい場合、値に transform を指定するのがおまじないだ。

.icon {
  animation-name: loop-icon;
  animation-duration: 60480ms;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}

古の -webkit-transform: translate3d(0, 0, 0) と変わらないじゃんと思うかもしれない。ハック感は否めないけど、でもいちおうオプトインだから。名前も「will-change」であくまで意図だしブラウザが今後もこの挙動とは限らないから(もごもご)……

レイヤーを確認する

レイヤーを切ったのはいいけれど、どう確認すればいいか。もいちどDevToolsのRenderingメニューを開く。そこにある「Layer Borders」をチェックすれば、レイヤーの境界にボーダーが引かれるので、どこにレイヤーがあるかがわかる。

以下はもともとのページのレイヤー。レイヤーの境界は薄いオレンジで、ページの端についている。ページ全体が一枚のレイヤーみたいだ。

f:id:myakura:20160513140733p:plain

水色の水平線が見える。これはタイルというレイヤーのいち領域の区切りを示している。
右と下に緑色の矩形が見えるけど、どうやらスクロールバーの領域もレイヤーの一種みたいだ。

以下は .iconwill-change: transform を与えてレイヤー化したもの。アイコン周りに新しいオレンジの枠ができたのがわかる。

f:id:myakura:20160513140745p:plain

レイヤー分けた効果を確認

ではタイムラインを録ってみよう。

f:id:myakura:20160513142111p:plain

ふむ。ジャンクはまだあるけど、描画の負荷がだいぶ減っている。1フレームとばしで60FPSも達成している。またちょっと進んだね。

f:id:myakura:20160513142126p:plain

あんなに太かったPaintもほっそくなって、描画領域も「1362 × 120」に、レイヤールートも狙い通り #document から div.icon になった。

しかしまだジャンクがあるのが気になる。確かに見るとまだガタガタしている。
あと、Paintingが減ってはじめて気になったのが紫色のRendering。毎回なにをレンダリングしているのか?そう、背景画像だ。

transform でするする

実は、CSSアニメーションにしたから負荷が減るというのは限定的な話で、プロパティによってはレイアウト、スタイルの計算、描画がはしってしまう。レイアウトはスタイル計算を、スタイル計算は描画を引き起こすので、できるかぎり前の段階からの最小化が望ましい。

プロパティごとに引き起こされるレイアウト・スタイル計算・描画は、Paul Lewisらがまとめている。

最近Blink以外のエンジンも頑張っている。
さて、background-position の項をみると、it will cause painting to occur とある。なので描画は結局おこってしまう。あとスタイルの計算もはしってしまう。なのでjQueryまわりのスクリプト・レイアウトの負荷がなくなるだけで、コストの掛かる描画は残ったままになる。

どうすればいいか。アニメーションによるのでどうにもできないケースもあるだろうけど、今回のアニメーションであれば transformtranslateX() に置き換えるといい。transform は描画も起こさないので、この負荷も下げられる。というわけでこちらにしよう。

背景画像の繰り返しを利用してずっと流れているように見せているので、書き換えはちょっと工夫が必要。今回は .icon の幅を広げて、それを translateX() で動かすようにした。

@keyframes loop-icon-transform {
  from { transform: translateX(0px) }
  to { transform: translateX(-1680px) }
}
.kintone {
  overflow: hidden;
}
.icon {
  width: 3360px !important;
  animation: loop-icon-transform 60480ms linear infinite;
}

ウインドウの幅が大きいと端が見えちゃうだろうけれど、だいたいカバーできるだろう。
もうちょっといいつくりがあるのかもしれない。translateX() で動かせて、切れ端が見えないのならなんでもいい。

なお、今回は will-change を指定していない。というのも、指定しなくてもレイヤーを分けてくれたから。will-change もこのアニメーションの自動レイヤーわけもブラウザ依存なので、対応環境によってはつけないといけないかもしれない。

ではタイムラインを録ってみよう。

f:id:myakura:20160513143936p:plain

ててーん。ジャンクなし。だいたい60FPS。RenderingとPaintingも消えた。
見た目もするするしている。するするさせられた!


というわけで、するするさせたいという目標はいちおう達成できたかな。どうぞご利用ください。

ただちょっとだけ気になることとか、あと少し書いときたいこともある。そいつはまた次回。

(追記):書いたよー

おまけ:試してみよう

どんなコードをコンソールにぶっこんだかちょっと書いとこう。おためしあれ。

background-position 版のCSSアニメーションに置き換え。

jQuery('.icon').stop().css('backgroundPosition', '')
document.head.innerHTML += `<style>@keyframes loop-icon { from { background-position: 0 } to { background-position: -1680px } } .icon { animation-name: loop-icon; animation-duration: 60480ms; animation-timing-function: linear; animation-iteration-count: infinite; }</style>`

will-change: transform はStyleパネルから追加している。クリックでオンオフさせやすいので。

以下は translateX 版のCSSアニメーション(最終版)に置き換えるもの。

jQuery('.icon').stop().css('backgroundPosition', '')
document.head.innerHTML += `<style>@keyframes loop-icon-transform { from { transform: translateX(0px) } to { transform: translateX(-1680px) } } .kintone { overflow: hidden; } .icon { width: 3360px !important; animation: loop-icon-transform 60480ms linear infinite; }</style>`

いま考えたらTemplate Strings使ってるんだからべつに改行取っ払うこともなかったな…