読者です 読者をやめる 読者になる 読者になる

青空コメントアウト

WEBのこと、デザインのこと、ご飯のこと、趣味のこと。青空の向こうの誰かに届きますように。

逆引き!フロントエンドのイマドキパフォーマンス改善

// WEB開発の話
このエントリーをはてなブックマークに追加

f:id:cocoro27:20170118182205j:plain

WebアプリにしろWebサイトにしろパフォーマンスはとても重要です。どんなに高機能であっても、どんなにデザインが良くても、パフォーマンスが悪ければユーザーは離れてしまいます。
とは言え現場はキツキツのスケジュールで、パフォーマンスにまでこだわる余裕がないよ!パフォーマンスはひとまずできる限りのところまで頑張るよ!となってしまうこともあるかと…。

この問題の解決の糸口はパフォーマンスを良くする手段をどれだけ知っているかです。仕様を決めるとき、デザインを決めるとき、実装するとき、それぞれのフェーズでパフォーマンスを常に意識していると自ずとハイパフォーマンスに近づきます。

というわけで今回はフロントエンドの観点から、イマドキのパフォーマンス改善手法をまとめてみました。イマドキと謳っておきながら2年前くらいの技術が出てきたりして最新の話題でもないのですが。

ちなみに、本題に入る前にWebパフォーマンスの基本として押さえておくべき本を紹介しておきます。どうぞ遠慮なく離脱してポチりに行ってくださいませ。

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

  • 作者: Steve Souders,スティーブサウダーズ,武舎広幸,福地太郎,武舎るみ
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/11
  • メディア: 大型本
  • 購入: 32人 クリック: 676回
  • この商品を含むブログ (127件) を見る

続・ハイパフォーマンスWebサイト ―ウェブ高速化のベストプラクティス

続・ハイパフォーマンスWebサイト ―ウェブ高速化のベストプラクティス


もくじ



リフローを減らす

  1. widthなどレイアウトに影響のあるプロパティ操作を最小限にする
  2. will-changeを節度を守って使う
  3. CSS Containmentを利用する

言わずと知れたリフロー問題です。1, 2の情報はそこそこ流通しているので、今回はwill-changeの注意点とCSS Containmentの概要について触れます。
リフローってなんだ?という場合はこちらの記事がとても参考になります。


will-changeプロパティ

軽くおさらいをすると、レイアウトをアニメーション付きで変更する動き(横からスライドインするハンバーガーメニューなど)はCPU負荷が高く、モバイル端末などではカクついてしまうことがあります。これを回避するために処理をGPUに任せる(≒GPUアクセラレーション)という手法があります。opacitytransformなどの特定のCSSプロパティは、ブラウザによってGPUアクセラレーションが対応されています。
そのため、上記の例のスライドインを実装するときはtransform: translateX()を使ったほうがいい、ということになるわけです。

/** CPUを使うのでカクついてしまう例 */
.menu {
  width: 100px;
  left: -100px;
  transition: left 0.4s ease;
  &.show {
    left: 0;
  }
}

/** GPUを使う例 */
.menu {
  width: 100px;
  transform: translateX(-100%);
  transition: transform 0.4s ease;
  &.show {
    transform: translateX(0);
  }
}


ただ、これは「このプロパティはブラウザがGPUで処理させるだろう」という期待を込めた実装であって、GPUアクセラレーションが確約されているわけではありません。そこで、明示的にブラウザにGPUアクセラレーションを指示する(というか、レンダリングの最適化を指示する)方法がwill-changeです。
仕様的な話をすると、要素がこれからどう変化するかを事前にブラウザに伝えることで、レンダリングを最適化させるというものになります。

これだけ言うと便利そうですが、こいつを扱うには知っておくべき注意点があります。

GPU利用はマシンのリソースを大量消費する

何でもかんでもGPUを使わせればいいというものではありません。GPUでの処理は高コストなのです。なので、基本的にはJavaScriptを利用して要素が変化する前にwill-changeを付与し、変化が終わったら取り除くということをする必要があります。

ブラウザは勝手に最適化するので利用はほどほどに

そもそもブラウザはレンダリングの最適化を行っています。その最適化に意図的に手を加えるのがwill-changeなので、ご利用は計画的にということです。

将来起きる変更に対して付与しないと効果がない

will-changeは名前の通り「将来起きうる変更」をブラウザへ伝えるもの。ブラウザはそれを知ってから最適化を行うので、あまりに直前will-changeを付与されても効果を出せません。以下の記事に例が記されていました。

例えば、要素がクリックされる時に変化するとしたら、その要素がホバーされる時にwill-changeを設定すれば、ブラウザが変化に備えて最適化する時間を稼げます。ユーザが要素にホバーしてから実際にクリックするまでの時間で、ブラウザは十分に最適化を行うことができるからです。

参考:CSS will-changeプロパティについて知っておくべきこと

f:id:cocoro27:20170118173129p:plain


CSS Containment

ブラウザが行うスタイルやリフローやペイントの範囲を制限するという新しい仕様です。言い換えると、CSSやJavaScriptで要素を変化させるときにレンダリングする範囲を制限させることができるということです。つまりリフローが減るということです!(とても喜ばしいですね🎉)
containというCSSプロパティを利用し、基本的には以下4つの値を設定することができます。

layout

要素に対しリフローの封じ込めを有効にする。つまりこの要素に対しリフローが起き得る変化を加えた場合に、本来ならページ全体に起きるリフローが、この要素の中でしか起きなくなる、ということです。

paint

要素に対しペイントの封じ込めを有効にする。

size

指定された要素が子孫要素の内容を調べずにレイアウトされる。つまり子要素の大きさに影響を受けないということです。子要素を描画しきらずとも、自身のサイズを決定できるのでその分リフローが減ることになります。

style

特定のスタイルの指定が外部の影響を受けない。また外部に影響を与えない。注意しないといけないのは、CSSにスコープを与えるわけではないということです。

参考:
CSS Containment Module Level 1
Chrome 52 に CSS Containment が導入

f:id:cocoro27:20170117184742j:plain



高負荷のペイントを回避する

これは単なる知見ですが、border-radiusbox-shadowを併用しないほうがいいというだけの話です。

参考:CSS Paint Times and Page Render Weight - HTML5 Rocks



優先度の低い処理を先送りにする

ブラウザはシングルスレッドであるため、どうしてもキューが溜まってしまい遅延の原因となります。そして溜まる多くがJavaScriptで書いた自前の処理(のはず)です。必要な処理を順番に実行しているのだからどうしたって時間がかかってしまうのですが、中にはあまり優先度の高くない処理もあるかと思います。それらの実行を後回しにする仕組みがrequestIdleCallbackです。

これを使うことでブラウザがアイドル状態になったら処理を実行することを指示できます。

let func = requestIdleCallback(() => {
    // 先送りにする処理
}, {
    timeout: 1000  // 最大でどれだけ先送りにするか
});


参考:requestIdleCallback - Web API インターフェイス | MDN

f:id:cocoro27:20170117184749j:plain



scrollイベントを減らす

少し前にパララックスを中心にスクロールエフェクトが流行ったり、スクロールに応じてlazyloadするチューニング法が出てきたりと、scrollイベントの利用頻度が高まりました。しかしscrollイベントを監視することはスクロールの滑らかさを犠牲にするので、悪手となる場面がままあります。この代替手段となるのがIntersection Observerです。

スクロール時に特定のDOMがスクリーン上に現れる(スクリーンとの交差を検出する)タイミングでイベントを発火してくれます。scrollイベントより軽量であり、scrollイベントを利用する多くの場面で代替手段となりそうです。

let option = {
  root: document.querySelector('#container'),  // 何を基準に交差検出するか
  threshold: [0, 0.2, 0.4, 0.6, 0.8, 1.0],     // 交差領域が20%変化する度にcallbackを呼ぶ
  rootMargin: '8px'                            // 上下左右が交差する8px手前でcallbackを呼ぶ
};

let observer = new IntersectionObserver((changes) => {
  for (let change of changes) {
    console.log(change.time);               // 変更が起こったタイムスタンプ
    console.log(change.rootBounds);         // ルートとなる領域
    console.log(change.boundingClientRect); // ターゲットの矩形
    console.log(change.intersectionRect);   // ルートとガーゲットの交差町域
    console.log(change.intersectionRatio);  // 交差領域がターゲットの矩形に占める割合
    console.log(change.target);             // ターゲット
  }
}, option);
observer.observe(document.querySelector('#target'));

optionはもちろん指定しなくてもOKです。

参考:Intersection Observer を用いた要素出現検出の最適化

f:id:cocoro27:20170117184744j:plain



scrollジャンク を減らす

モバイルなどでよく起きるスクロールの詰まりのこと。 スクロールのイベントハンドラの中でpreventDefault()が呼ばれた場合、ブラウザはスクロールを止めなければなりません。そのため、ブラウザはイベントハンドラの中で「preventDefault()が呼ばれないこと」を確認するまではスクロールを一時停止しておく必要があるわけです。これがscrollジャンクです。

これを改善するものがPassive EventListenerです。 preventDefault() が呼ばれないことを保証する機能を addEventListener() に加えたものです。これを使うことでブラウザはスクロールを止めることなくイベントハンドラの処理をさばけます。

document.addEventListener('touchstart', handler, {passive: true});

addEventListenerの第3引数に{passive: true}を追加するだけです。既存の実装に手を加えるのも簡単です。


参考:Passive Event Listeners によるスクロールの改善

f:id:cocoro27:20170117184745j:plain



リソース取得を最適化する preload 編

現在表示しようとしているページに必要なリソースの取得を最適化する方法のひとつがpreloadです。これはあるページをレンダリングする過程で、「後から取得されるがレンダリングする上で重要なリソース」を先に取得するよう制御するものです。簡単に言えばリソースを予め取得するということですね。

<link rel="preload" as="image" href="image.png">

個人的に、preloadの良さは「応用が効く」という点だと思っています。単にリソースを先に取得するだけでなく、自身でonloadを発火するためにローダーとして利用することもできます。その他いろいろな利用方法がまとめられている記事がありました。

参考:Preload を用いたリソースプリローディングの最適化

f:id:cocoro27:20170118183622p:plain



リソース取得を最適化する Resource Hints 編

Resource Hintsとは、link要素にdns-prefetchpreconnectprefetchprerenderを指定することで、遷移先のページで必要になるリソース取得のプロセスを最適化することがでるAPI群です。これにより画面遷移にかかる時間を短縮できます。

preloadとの違いは、preloadが現在描画しているページのリソースのコントロールであるのに対し、Resoure Hintsは将来遷移するであろうページのリソースに対する操作だという点にあります。

活用場面例
・ログイン前にログイン後の画面に必要なリソースを取得しておく
・フォームの確認前に確認画面で必要なリソースを取得しておく

おさらい
リソース取得のためにはサーバへリクエストを投げる必要があるわけですが、そのプロセスはざっくりとこんな感じです。

  1. URLを元にどのサーバに問い合わせればいいかを判断する
  2. サーバへリクエストを投げるために接続を確立する
  3. サーバへリクエストをぶん投げレスポンスとしてリソースを受け取る

これを踏まえて、4つのパラメータがどう作用するか見ていきます。


dns-prefetch

DNSルックアップを事前に行いキャッシュしておき、名前解決コストを下げる。 プロセス①を事前にしておくということです。

<link rel="dns-prefetch" href="//exsample.com">

f:id:cocoro27:20170117184743j:plain


preconnect

DNSルックアップとTCP接続を事前にしておく。クロスオリジンでもできちゃう。 プロセス②までを事前にしておくということですね。

<link rel="preconnect" href="//exsample.com" crossorigin>

f:id:cocoro27:20170117184746j:plain


prefetch

リソースを事前に取得しておきブラウザにキャッシュしておく。 プロセス③までを事前に(略)。
静的なファイルで中身が変わらないもの、という保証があるときに使うべきです。

<link rel="prefetch" href="//example.com/resources/img/hoge.jpg">

f:id:cocoro27:20170117184747j:plain


prerender

全てのリソースがprefetch可能という場合に、全部prefetchしておいてページ丸ごとバックグラウンドで描画までしておく。丸ごとってすごいですね…。

<link rel="prerender" href="//exsample.com/page.html">

f:id:cocoro27:20170117184748j:plain



さいごに

要所要所でブラウザの対応バージョンを載せましたが、随時変わるものなので実践前には今一度確認を。

最後になりますが、パフォーマンスへのこだわりはユーザーへの愛だと思っています。1msレスポンスを速くすることは、デザインや機能を良くすることに比べれば地味でリターンも少なく感じるかもしれませんが、それをどれだけ追求するかがユーザーへの愛の深さ、まごころの深さの現れといえるのではないでしょうか。まごころを、君に

TOP