このページの本文へ

ハイパフォーマンスCSS3アニメーション——60fpsを実現するベストプラクティス

2017年03月07日 03時00分更新

文●Jose Rosario

  • この記事をはてなブックマークに追加
本文印刷
すっかり普及したCSS3アニメーション。ハイパフォーマンスなアニメーションを実現する、ちょっとした書き方のポイントとは?

モバイルアプリケーション内の要素をアニメーションさせるのは難しいことではありません。でも、この記事を読めば、さらに適切な方法でアニメーションさせることができます。

最近、多くの人がモバイルでCSS3アニメーションを使っていますが、正しく使っていない場合が多くあります。開発者はしばしばベストプラクティスを無視します。なぜなら、多くの人はなぜCSS3アニメーションが存在し、なぜ急速に浸透しているのかを理解していないからです。

モバイルデバイスのスペックの幅はとても広いので、もしコードを最適化しなければ、シェアが高い端末では標準以下の体験しか提供できないでしょう。

注意:いくつかのハイエンドなデバイスは可能性の限界に挑んでいますが、世界中の多くの人はそうしたスペックモンスターに比べると非常に遅れたデバイスを使っています。

これからCSS3の正しい使い方について紹介します。最初に、いくつかの点を理解する必要があります。

タイムラインを理解する

ブラウザーはレンダリングして要素を動かす間に、なにをしているのでしょうか。このタイムラインは、クリティカルレンダリングパスと呼ばれています。

Critical Rendering Path

参照元:www.csstriggers.com

なめらかなアニメーションを実現するために、コンポジットステップで要素に変化を加えます。

1. スタイル

Styles

ブラウザーは、要素に合ったスタイルを計算します。スタイルの再計算です。

2. レイアウト

Layout

以降のレイヤーで、ブラウザーは要素の位置や形をつくります。レイアウトです。例では、ブラウザーがwidthheightmarginleft/top/right/bottomといったページ要素を即座にセットします。

3. ペイント

Paint

ブラウザーはレイヤーの各要素のピクセルを描画します。プロパティで言うと、box-shadowborder-radiuscolorbackground-colorなどが該当します。

4. コンポジット

この段階がアニメーションを実行するステップです。ブラウザーがすべてのレイヤーを描画し終わっているからです。

Composite

モダンブラウザーはtransformopacityプロパティを使って、次の4つの要素をうまくアニメーションにします。

  • 位置 — transform: translateX(n) translateY(n) translateZ(n);
  • 大きさ — transform: scale(n);
  • 回転 — transform: rotate(ndeg);
  • 透過度 — opacity: n;

秒間60フレームを実現する方法

これで準備が整いました。これから実際にアニメーションさせる方法を紹介します。

最初に、HTMLです。とてもシンプルな構造をつくり、app-menuというクラスを追加します。

<div class="layout">
  <div class="app-menu"></div>
  <div class="header">
    <div class="menu-icon"></div>
  </div>
</div>

app-menu inside layout class

誤った方法

.app-menu {
  left: -300px;
  transition: left 300ms linear;
}

.app-menu-open .app-menu {
  left: 0px;
  transition: left 300ms linear;
}

変更したプロパティが分かりますか? left/top/right/bottomプロパティをアニメーションで使うのは避けるべきです。ブラウザーにレイアウトパスを再生成させ、子の要素にも影響を与えてしまうため、なめらかなアニメーションをつくれないからです。

このような結果になります。

Avoid transitions

見ての通り、なめらかではありません。なにが起きているかを知るためにデベロッパーツールのタイムラインを確認します。以下がその結果です。

DevTools Timeline

緑のエリアがアニメーションのレンダリングにかかっている時間です。

フレームレートが不規則で、パフォーマンスも低くなっています。

緑のバーはfpsを示している。高いバーはアニメーションが60fpsを超え、低いバーは60fps未満を意味している。つまり、緑のバーを絶え間なく高く保つことが理想だ。赤いバーはジャンクを意味し、これをなくすことが改善の指標になる。

Kayce Basquesありがとう!

Transformを使う

.app-menu {
  -webkit-transform: translateX(-100%);
  transform: translateX(-100%);
  transition: transform 300ms linear;
}
.app-menu-open .app-menu {
  -webkit-transform: none;
  transform: none;
  transition: transform 300ms linear;
}

transformプロパティは、レイアウトではなくコンポジットステップに影響を与えます。コンポジットステップは安定していて、レンダリングするときに支障が出にくいため、アニメーションを実行するのに最適です。

Using Transform

タイムラインには以下のようになります。

Timeline using transform

先ほどと比べて改善されました。フレームレートが安定し、アニメーションもなめらかに動きます。

GPUでアニメーションを実行する

さらに掘り下げていきます。本当になめらかなアニメーションはGPUでレンダリングします。

.app-menu {
  -webkit-transform: translateX(-100%);
  transform: translateX(-100%);
  transition: transform 300ms linear;
  will-change: transform;
}

いくつかのブラウザーではまだtranslateZ()translate3d()に頼っていますが、今後はwill-changeの使用が期待されています。このプロパティは要素を別のレイヤーに変化させるため、ブラウザーはレイアウトのレンダリングやペインティングを考慮する必要がありません。

Animations in GPU

なめらかなのが分かりますね。タイムラインは次のようになります。

Timeline running animations

アニメーションのフレームレートがさらに安定して、レンダリングも早くなっています。しかし、まだ最初に長いフレームがあります。

最初につくったHTMLの構造を覚えていますか。app-menuのdivをJavaScriptでどのようにコントロールしたか確認します。

function toggleClassMenu() {
  var layout = document.querySelector(".layout");
  if(!layout.classList.contains("app-menu-open")) {
    layout.classList.add("app-menu-open");
  } else {
    layout.classList.remove("app-menu-open");
  }
}
var oppMenu = document.querySelector(".menu-icon");
oppMenu.addEventListener("click", toggleClassMenu, false);

そうです! layoutのdivにクラスを追加したことが原因でした。ブラウザーがスタイルツリーを再生成しなければならず、レンダリングパフォーマンスにも影響しているのです。

60fpsのための、さらに良い解決策

viewportエリアの外側にメニューを作ったらどうなるでしょう。隔離したエリアにメニューがあると、アニメーションにする要素にのみに作用します。

HTMLの構造は以下のようになります。

<div class="menu">
  <div class="app-menu"></div>
</div>
<div class="layout">
  <div class="header">
    <div class="menu-icon"></div>
  </div>
</div>

少し違った方法でメニューをコントロールする必要があります。 JavaScriptのtransitionend関数を使って、アニメーションを操作し、アニメーションが終了したときに取り除きます。

function toggleClassMenu() {
  myMenu.classList.add("menu--animatable");        
  if(!myMenu.classList.contains("menu--visible")) {                
    myMenu.classList.add("menu--visible");
  } else {
    myMenu.classList.remove("menu--visible");                
  }        
}

function OnTransitionEnd() {
  myMenu.classList.remove("menu--animatable");
}

var myMenu = document.querySelector(".menu");
var oppMenu = document.querySelector(".menu-icon");
myMenu.addEventListener("transitionend", OnTransitionEnd, false);
oppMenu.addEventListener("click", toggleClassMenu, false);
myMenu.addEventListener("click", toggleClassMenu, false);

ここまでのすべてを実装した結果を確認してください。CSS3を正しく使って、その能力を最大限に引き出した例です。

.menu {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  z-index: 150;
}

.menu--visible {
  pointer-events: auto;
}

.app-menu {
  background-color: #fff;
  color: #fff;
  position: relative;
  max-width: 400px;
  width: 90%;
  height: 100%;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
  -webkit-transform: translateX(-103%);
  transform: translateX(-103%);
  display: flex;
  flex-direction: column;
  will-change: transform;
  z-index: 160;
  pointer-events: auto;            
}

.menu--visible .app-menu {
  -webkit-transform: none;
  transform: none;
}

.menu--animatable .app-menu {
  transition: all 130ms ease-in;
}

.menu--visible.menu--animatable  .app-menu {
  transition: all 330ms ease-out;
}

.menu:after {
  content: '';
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.4);
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
  transition: opacity 0.3s cubic-bezier(0,0,0.3,1);
}

.menu--visible.menu:after {
  opacity: 1;
  pointer-events: auto;
}

Fully enabled CSS3 example

タイムラインはどうなったでしょうか。

Timeline of CSS3 example

緑のバーが絶えず表示されています。デモはこちらです。

※この記事はOutSystemsの記事を元にしています。SitePointに協力してくれた人びとに感謝します。

(原文:Achieve 60 FPS Mobile Animations with CSS3

[翻訳:萩原伸悟/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事