こんにちは、ふろしきです!
私はHTML5 Experts.jpで、過去2年ほどGoogle I/Oの情報を発信し、Web技術の変化についてお伝えしてきました。振り返るとGoogleは、2014年にモバイルWebの提唱と技術要素の拡大を図り、2015年からは「RAIL(モバイルWebが目指すべきパフォーマンス指標)」や「Progressive Web Apps(アプリのように振る舞うWeb)」といった、モバイルとの親和性が高いWebを作り出すための”考え方”を推し進めました。今年2016年は、さらにそれを踏み込んでいったという感じがします。
今回のI/Oで取り上げるのもそのひとつ。毎度お馴染みGoogle Developer AdvocateのPaul Lewis氏による 「High performance web user interfaces」です。彼は、モバイルにおいて、時にアプリのように振る舞うことが求められる昨今のWeb、すなわち「Progressive Web Apps」について、UIで起こりがちなパフォーマンス問題と、その改善方法について紹介しています。
※ この講演、動画無しでは説明が難しかったり、前提知識も多かったりするので、私でかなりアレンジ・要約して紹介しています。より詳細に内容を知りたい場合は、ソースをみることをオススメします!
Webは時として、モバイルアプリのような体験が求められる
モバイルにおいて、ホームスクリーンは重要な場所だ。人々はホームスクリーンから、目的を達成するためのアプリを起動する。Webは、Add to Homescreenを使うことで、ホームスクリーンからWebサイトへアクセスすることができるようになった。
するとどうなるか。このホームスクリーンをよくみてほしい。どれがWebで、どれがネイティブアプリなのかは見分けがつかないだろう。Google Mapsなんかはネイティブにみえるけれど、他はまったく想像がつかない。しかしこれらが、Google Mapsと同様にネイティブアプリにみえるなら、Webはネイティブアプリのように振る舞うことが求められている。
パフォーマンスモデル、インタラクションモデルの2つによって、Webはモバイルネイティブアプリのような振る舞いをえることができる。Progressive Web Appsを実現することができる。今日はこの2つのモデルのうち、パフォーマンスモデルの話をしたい。
昨年は、Paul IrishやIlya Grigorikなどの私のチームのメンバーが、「RAIL」というパフォーマンスモデルについて話した。RAILとは、Responseは0.1秒、Animationは16ミリ秒、Idleは50ミリ秒、Loadは1秒で動作すべきというもの。ただ、それを聞いた人々は、たまに勘違いをする。この4つの要素は、どれも全て、最も重要なこととして語ってしまうのだ。それは間違っている。
例えば、Webサイトにおいて、タップした時に求められるのは、4つの要素のうちLoadが重要になる。Idleが重要になることはそこまでない。そして、ホームスクリーンからタップして起動されるProgressive Web Appsでは、ResponseやAnimationが重要になる。Webサイトをつくるのと、Progressive Web Appsをつくるのでは、求められることが違う。
さて、このようにパフォーマンス面で求められることが異なるProgressive Web Apps。そこに、3つのコンポーネントがある。Side Navigation、Swipeable Cards、Expand an Collapse。これらを実現するセオリーを紹介しよう。
1. Side Navigation
まずは、このコンポーネント。メニューボタンをタップすると左からスライドインするバー。これは、2つのElementによって構成される。半透明の黒い背景と、サイドメニューを表示する領域だ。
このサイドメニューの部分のCSSは非表示の時、CSSにpointer-events: none;
を指定する。そして、表示されたタイミングでpointer-events: auto;
を指定する。
そしてここからが大事な話。左から右、あるいは右から左に移動させる際に、transformを使う。ブラウザがDOMの位置を変更する際に、CPUを使ったレイアウト変更してはいけない。GPUの力を借りて、描画位置を変更することで、最適なパフォーマンスを得ることができる。
例えば、一昔前。サイドメニューが左に消えている時にCSSは
1 2 3 4 5 6 7 8 9 |
.side-nav { position: fixed; left: -102%; /* DOMのレイアウト位置を左にずらしてメニューを隠す */ top: 0; width: 100%; height: 100%; over-flow: hidden; pointer-events: none; } |
と、left: -102%
で隠す。これは一般的な方法だった。しかし、描画を高速に処理できるGPUの恩恵を受けたいなら、transformを使って以下のように記述する。
1 2 3 4 5 6 7 8 9 10 11 |
.side-nav { position: fixed; left: 0; /* DOMのレイアウト位置は常に0のまま */ top: 0; width: 100%; height: 100%; over-flow: hidden; pointer-events: none; transform: translateX(-102%); /* 描画の位置を左にずらすことでメニューを隠す */ will-change: none; /* <- これは何!? */ } |
サイドメニューのDOMのレイアウト位置としては、x位置のleft
もy位置のtop
も、0のまま。横幅width
も縦幅height
も、100%ということで、全面を覆っているという扱いになる。しかし、transform: translateX(-102%);
で描画の位置自体を、左に寄せている。
そして、ここで登場するのがwill-chanage: none;
だ。
一昔前にtransform: translateZ(0);
をCSSプロパティに指定して、パフォーマンスを改善するというハックが出回ったのをご存知だろうか。このCSSが指定されると、描画には必然的にGPUの力が必要になるため、強制的にGPUに描画を依頼することになる。GPUの恩恵を受けるために活用されたこのバッドノウハウは、will-chanage: transform;
という新しいCSSプロパティをWeb標準として追加することによって、同様のことを実現できるようにした。(※注:実態はブラウザ対応の問題もあり、今もtransform: translateZ(0);
を使うのが一般的)
ただ、transform: translateZ(0);
やwill-chanage: transform;
といったCSS指定は、常時ビデオカード上のRAMメモリーに描画結果をテクスチャーとして保存することになる。モバイル環境では、バッテリー消費などに悪影響を及ぼすことになる。動作するタイミングだけwill-chanage: transform;
を指定し、動作しない時は無効化will-chanage: none;
するといい。これが、バッテリー消費パフォーマンスと描画速度パフォーマンスのトレードオフ問題に対する、落とし所だ。
黒背景については、will-change: opacity;
というプロパティがあり、transformと同様の方法で、高いパフォーマンスで描画させることができる。(※ JSの実装については、「2. Swipeable Cards」にノウハウが似ているので割愛)
2. Swipeable Cards
CSSを使ったパフォーマンス改善のテクニックの他に、注意しなくてはいけないのが、スワイプ操作時のコンポーネントの移動処理。ユーザーからの指の位置状況を入力し、それをスクリーン上に反映しなくてはいけない。この際、有用なのが「ゲームループ」のノウハウだ。
描画のイベントは常に、1/60秒ごとに発生する。対してスワイプのイベントは、常に一定には発生しない。描画のタイミングにはあわせてくれないのだ。
そこで、スワイプにより発生するイベントについては、変数に位置情報だけを記録する。そして、描画時のイベントでは、記録された位置情報を元に、CSSを通じて描画位置変更をおこなう。
スワイプの開始時・移動時・終了時は以下の通り。this.startX
、this.currentX
、this.targetX
といった変数に、現在の位置や、移動すべき位置を記録している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/** * スワイプ開始 */ onStart(evt) { // スワイプの開始位置を記録する this.startX = evt.pageX || evt.touches[0].pageX; this.currentX = this.startX; // cardの移動が開始されたことを記録する this.draggingCard = true; // will-change: transform; を有効にする this.target.style.willChange= ‘transform’; // カード上の要素にイベントを伝播させないように evt.preventDefault(); // アニメーションを開始する requestAnimationFrame(this.update); } /** * スワイプ移動時 */ onMove(evt) { // スワイプの現在地点を記録する this.currentX = evt.pageX || evt.touches[0].pageX; } /** * スワイプ終了時 */ onEnd(evt) { // cardを削除すべきかどうか判定する let translateX = this.currentX - this.startX; const threshold = this.cardWidth * 0.35; if( Math.abs(translateX) > threshold ) { // cardの移動先をスクリーンの外へ(※cardは削除) this.targetX = (translateX > 0) ? this.cardWidth : -this.cardWidth; } else { // cardの移動先を最初の位置へ(※cardは削除されない) this.targetX = 0; } // cardの移動が終了されたことを記録する this.draggingCard = false; } |
描画のタイミングにrequestAnimationFrameから呼び出されるコールバックで、先ほどの位置情報を元に反映していく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * 描画内容の変更 */ update(evt) { // 次の描画タイミングでも自身を呼び出す requestAnimationFrame(this.update); // スワイプ中の場合 if( this.draggingCard ) { // 現在の位置を描画させる this.translateX = this.currentX - this.startX; // スワイプが完了している場合 } else { // カードを削除するかしないかに応じて指定の場所に能動的に移動する this.translateX += (this.targetX-this.translateX)/4; } // CSSプロパティを経由してGPUに変更を伝える this.target.style.transform = `translateX(${this.translateX}px)`; } |
(※ この後の処理については、「3. Expand and Collapse」にノウハウが似ているので割愛。)
3. Expand and Collapse
タップすると、領域が広がり全体化されるUIコンポーネント。CSSではどうするのか?もちろん、ここまで説明してきた「transform」を活用する!では、JSについてはどうか?実は、「2. Swipeable Cards」とは異なり、スワイプ操作でなくタップによって、自動的にアニメーションする。この点で、より効率的な実装が求められる。
まず、アニメーションについて、動作中の状態はJS上で持たない。動作前後の状態だけを、CSSプロパティを通じてGPUに指示する。
1 2 3 4 5 6 7 8 9 10 11 |
// 変化量を計算する invert.x = first.left - last.left; invert.y = first.top - last.top; invert.sx = first.width / last.width; invert.sy = first.height / last.height; // 変化後の状態をCSSプロパティを通じてGPUに指示 card.style.transformOrigin = ‘0 0’; card.style.transform = `translate(${invert.x}px, ${invert.y}px) scale(${invert.sx}, ${invert.sy})`; |
そのままでは、タップした要素は一瞬にして全体化されてしまう。どのようにして何ミリ秒もかけて徐々に広げていくか?その方法は、CSSで指定する。JSではない。原理的には、従来よく使われているCSSアニメーションだ。
1 2 3 |
.cards { transition: transform 0.2s cubic-bezier(0,0,0.3.1); // アニメーションさせる } |
ここまで、Progressive Web Applsのパフォーマンス改善の話をしてきたが、「Google DevelopersのRendering peformance」が役に参考になる。一読するといいだろう。
Progressive Web Appsのパフォーマンス改善。要はこう言いたかった
いかがでしたでしょうか?文字数の制限やコンテキストの高さもあり、多くのエンジニアに伝わるようかなりアレンジしてみましたが、ご理解いただけましたでしょうか?
Paul Lewis氏が言いたかったことは単純な話です。先ほどのGoogle Developersの記事にもありますが、Progressive Web AppsにおけるAnimationやReactionの課題は、いかにしてブラウザのレンダリング処理における「レイアウト」を減らすか、という話です。この講演は、そのTIPS集といえます。
今日のノウハウ、特に新しいというわけでもなく2年前には既に実践されていたことです。実際のところ多くの現場では、OnsenUIやIonicのようなUIライブラリを活用することになり、このあたりの話を意識することはないのでしょう。ただ、Webのサービスを作っているフロントエンドエンジニアにとっては、ライブラリの有無に関係なく知っておくべき知識のように思えます。サイドメニューについては、Webサイトであっても鉄板のUIコンポーネントなので、Progressive Web Appsか否かはもはや関係ないノウハウだったに違いありません。
Webがモバイルに順応していくことは、今後もさらに求められていきます。これは、フレームワークやライブラリに限った話ではなく、トータルにみたWeb、フロントエンドへの要求に変化を与えるに違いません。
今後も、モバイルとWebの関わりに、目が離せませんね。