概要
Reactを用いたWebアプリケーションにおけるアニメーションやインタラクションの実装について個人的なメモ。
これがベストプラクティスであるとは断言できないが、自分なりに試行錯誤した末の結論をまとめておく。
React/Redux特有のパフォーマンス的な問題を解決
- React/Reduxは確かに便利で高パフォーマンスだが、リアルタイムなインタラクションには弱い
- リアルタイムなインタラクションとは
onMouseMove
やonDrag
onScroll
などのイベントで発火する動作 - 上記のようなイベントを扱うケースでは
state
やprops
を使ってはいけない(カクカクしたりぎこちない動きになる) - 代わりに、状態管理にはJSの変数やプロパティなどを使うようにするとスムーズに動く
- とはいえオレオレフレームワーク化しがちなので、そういった実装は最小限に留める
stateやpropsを使わずにアニメーションさせるには
- refで取得したDOM要素を直接操作する
render()やcomponentWillReceiveProps()等の中で計算処理をしない
-
_.find()
やmoment().isSame()
といったよく使われる処理を、renderの中で配列で繰り返すと重い - render以外の場所(reduxならactionCreatorやreducer等)でやるべき
- util化して別ファイルから読み込んだとしてもrender()で実行するなら結局意味がないのでNG
- render()やcomponentWillReceiveProps()は静的な値を使うだけにするのがベスト
ReactでCSSアニメーションをカンタンに扱える公式ライブラリ
- react-transition-group というReact公式ライブラリを使うと平和が訪れる
- 簡単な使い方解説
<Transition in={trueOrFalse} timeout={500}>
{(state)=>(
<div className={classNames(styles.foobar, styles[state])}>
ほげほげ
</div>
)}
</Transition>
-
in
(boolean)
in属性にtrueを代入すると、引数stateがenteringに変わる -
timeout
(number)
CSS Transitionのduration(アニメーションの長さ)と同義。 -
state
(string)
アニメーションの状態-
"entering"
in==falseからtrueへのアニメーション中 -
"entered"
in==falseからtrueへのアニメーション完了後 -
"exiting"
in==trueからfalseへのアニメーション中 -
"exited"
in==trueからfalseへのアニメーション完了後
-
気持ちよいアニメーションを実現させるバウンスやイージングのコツ
画像を載せるのが面倒なので雰囲気で解説
/* スッキリシンプルなイージング */
.easing1 {transition-timing-function: ease}
/* 今風な気持ちいいイージング */
.easing2 {transition-timing-function: cubic-bezier(1,0,0,1)}
/* 最後に軽くバウンス */
.bounce1 {transition-timing-function: cubic-bezier(0.9,0,0,1.1)}
/* 最後に思いっきりバウンス */
.bounce2 {transition-timing-function: cubic-bezier(0.5,0,0,1.8)}
- もっとこだわりたい場合はこのWebツールが便利
http://cubic-bezier.com/
アニメーションは出来る限りCSS Transitionを使う
- Webブラウザでアニメーションさせる方法は色々ある
- JS(setTimeout/setInterval)
- SVG
- CSS Transition
- CSS Animation
- Canvas
- WebGL
- Animated GIF
- それぞれ長所短所がある
- 一般的にはCSS Transionを使うのが無難
- 理由①:CSSのプロパティを変更するだけでアニメーションしてくれる手軽さ
- 理由②:仕様がシンプルなので学習コストが少ない
- 理由③:react-transition-groupを使うことでアニメーション前後の挙動も制御しやすい
アニメーションと相性の良いCSSプロパティ
- 結論から言うと、opacityかtransformだけをアニメーションさせれば幸せになれる
- それら以外のプロパティをアニメーションさせると重い(最悪の場合ブラウザが落ちる)
- ブラウザの画面描画の仕組み
-
Layout
(最も重い)
レイアウト処理(関連プロパティ:width, height, margin, padding, top, font-sizeなど) -
Paint
(やや重い)
描画処理(関連プロパティ:color, background, border-radius, box-shadowなど) -
Composite Layers
(軽い)
レイヤー合成処理(関連プロパティ:opacity, transformなど)
-
Layout
- Layout → Paint → Composite Layersの順番で処理が実行される
- つまり、
- Layout系プロパティを変更すると、LayoutとPaintとComposite Layersの再計算が実行されるため重い
- Paint系プロパティを変更すると、PaintとComposite Layersの再計算が実行されるため少し重い
- Composite Layers系プロパティを変更すると、Composite Layersのみ再計算されるため軽い
positionプロパティは取り扱い注意
-
position:absolute
を指定した要素を便宜上 絶対座標要素 と呼ぶ - 絶対座標要素の先祖要素の中で
position:absolute
またはposition:relative
を適用された最も近い親等の要素を便宜上 親要素 と呼ぶ - シームレスなインタラクションを実現する上で、親要素との関係性は非常にデリケートなので最大限注意しなければならない
- また、position指定をするとレイヤを新規生成することになり、レイヤの作りすぎはパフォーマンスの低下に繋がるため十分注意する
要素を重ねてアニメーションさせるテクニック
- シームレスでスムーズなインタラクションを実現する上で、アニメーション対象プロパティとしてwidthやheightなどを使えない(使いたくない)シーンがある
- そういう時は変形前と変形後の要素を別々に用意し、opacityとtransformでクロスフェード(フェードインする要素とフェードアウトする要素を重ねるアニメーション)をさせると吉
- ただ、座標の計算ロジックを作る工数が地味にかかるので、多用しないほうが吉
検証中の項目
- iOS Safariでブラウザが落ちるケースがある
- DOM要素が多すぎるせい?
- ブラウザ起動直後に再現しやすい