Lighthouseを使ってVue.jsで書いたポートフォリオサイトを爆速化する
本記事はVue.js Advent Calendar 2018の第3日目の記事です。
はじめに
GCPの無料枠が終了したため、ポートフォリオサイトはクローズドしましたが、引き続き本記事はご覧いただけます。
私は以前、インターンシップなどの選考に備えて自分のポートフォリオサイトなるものを作りました。
しかし、あまりに速くデプロイしたかったことからパフォーマンスについて一切無視した結果、とてつもなく遅いサイトができてしまいました。
結局、他人に見せるには恥ずかしいサイトになってしまい、今回の記事で爆速にできればと思います。
サイトはこちらで、SEO対策やOGPも実装したので以下のように展開されます。
要約
- 自分のポートフォリオサイトがロードまで8秒近くかかっていたのがキャッシュ込みで0.3秒くらいになった
- クリティカルレンダリングパスを知ることが重要
- Chrome DevToolを使い倒すとよくわかる。Lighthouseは便利だし、解決策をくれる
- いろんなサイトをみても最初は画像の質が悪い(サイズが大きいなど)の問題が目立つので注意する
サイトが遅くなる主な原因
ネットワークに関することです。ブラウザでコンテンツをリクエストすると、ネットワークを介してレスポンスが返ってきます。通信速度などの影響を受けるので、速度計測ツールなどによって検証が必要です。
レンダリングに関することです。画像などのデータはラスタライズされて表示されますが、一例として、画像が非常に大きいと、その時間もかかってしまいます。特に、リッチなアニメーションを導入していると、ボトルネックになりがちです。レンダリングの効率について検証する必要があります。
スクリプトに関することです。これは主にJavascriptを指します。特に、ロジックについては注意が必要です。 使用メモリなどを検証する必要があります。
0. 実際に遅いのかを定量的に調べる
まず、自分のサイトがどのくらいの時間でレスポンスを返すのかを調べます。
実際にどのくらい遅いのかを数字で知ることが重要です。
Google Chromeの「デベロッパーコンソール」のNetworkタブで調べることができます。私は普段Google Chrome Canaryを使っているので、興味がある人は使ってみてください。
ここでキャッシュがない状態で設定したいので、Disable Cacheにチェックボックスをいれます。
すると計測結果が下に表示されます。
めちゃめちゃ遅いです、、、 そもそも遅くても1.0秒が閲覧者のUXにもいいということなので、至急改善しないといけません。
また、全体的なサイトの評価は、"Audit"タブで測定することができます。
ターゲット端末、指標、通信状態を決めて評価します。20秒ぐらいかかりました。
まず、最初にモバイル端末から調査しました。
やはり、パフォーマンスが非常に悪いです。
しかしながら、タグを適切に使うなど、SEO対策はしたつもりだったので、かなりスコアはいいです。
提案は以下の通りです。
ネットワークのペイロードに問題があるみたいです。
デスクトップも調査してみると
同様の結果です。これから
- ネットワークに関して
- レンダリングに関して
- スクリプトに関して
の3つのセクションで解説していこうと思います。
1. ネットワークに関して
まず、ネットワーク処理の3原則は以下の通りです。
- データの転送量は小さく
- データの転送回数は少なく
- データの転送距離は短く
またブラウザの内部構造を知る中でもクリティカルレンダリングパスは非常に重要です。
1.0 クリティカルレンダリングパス
クリティカルレンダリングパスでは最初にHTMLをダウンロードして、バイトを解析し、トークンに変換し、DOMツリーというものを構築します。
またhead
などのlink
属性に
<!doctype html> <html> <head> <link href="main.css" rel="stylesheet"> ...
のようなstyle
があるとそれをネットワークから取得してCSSOMツリーを構築します。
そして最終的にはDOMツリーとCSSOMツリーを一つにレンダリングするわけです。これでできあがったものをレンダリングツリーと呼びます。
レンダリングツリーは構造が知れても、端末内でどのように表示するかはわかりません。つまりwindow
などがわからないということです。
次のレイアウト段階で決まります。このレンダリングツリーを実際のピクセルに変換することをペインティングやラスタライジングと呼びます。
ここで見えたDomContentLoaded
は青線で表示され、DOMツリーが構築された状態も示します。
またonload
は全てのリソースの処理が終了するまでの時間を示していて、赤線です。
後から参考画像を追加したのですでに爆速化し終わったものですが、このような対応関係です。
幸いにもリソースによってそこまで大幅にレンダリングがブロックされてはいませんでした。
今回はなかったのですが
青線と赤線の差が短いのならば何かしらブロッキングされている
ということですので、注意してみてください。
ここで少し体系的にクリティカルレンダリングパスを説明します。
仮に以下のようなレンダリングパスをとるページがあるとしましょう。あくまでもHTMLだけのページです。
このときはT0
とT1
はネットワークから取得するまでの時間なので、あまりアプリケーションエンジニアはすることはありません。
厳密に言えばデータの転送距離は短くの原則から近くのリージョンのデータセンターにコンテンツを置いたりすることで高速化できます。
次に新たにCSSを追加したとします。
HTMLとCSSで二回取得が必要になります。
ここからどんどんクリティカルレンダリングパスが何なのか?ってことがわからなくなるので、ここで用語を定義したいのですが
- クリティカルリソース: 初回のページレンダリングをブロックする可能性のあるリソース(cssやjs、画像ファイルなど)
- クリティカルパス長: すべてのクリティカルリソースを取得するために必要なラウンドトリップ数または総時間
- クリティカルバイト :初回のページレンダリングに必要なバイト数
です。デフォルトではHTMLもCSSもクリティカルリソースです。だってHTMLがダウンロードされるまで何も見えないからです。
またCSSも同様で、CSSOMが構築されるまでは処理済みコンテンツのレンダリングを保留します。
できるだけ小さくして、できるだけ早くクライアントに配信しましょう
ここでまたjavascriptファイルがあったとしましょう。
クリティカルリソースであるjsファイルはJavascriptを実行するためにブロックしてCSSOMを待つ必要があります。
scriptタグにasync
を追加すると
になり非同期スクリプトは、パーサーブロックをしないためクリティカルリソースではなくなります。またCSSも特定のページしか使わないのならば以下のようにレンダリングをブロックすることもないでしょう。
これはCSSにもいえて、例えばモーダルが使われていて、それに使うCSSならモーダルが出された時にロードする方が初期ページレンダリングをはやくすることができます。
クリティカルレンダリングパスを知ることで、ユーザーにすばやくコンテンツを返すことができるようになると思います。
つまりクリティカルレンダリングパスとネットワークの原則を合わせると
- データの転送量は小さく => ブロッキングするかもしれないので細か渡したり、何かしらはやく届けようとする
- データの転送回数は少なく => 何回も何回も取得するとそれが律速になってしまうので、できるならばまとめる
- データの転送距離は短く => 転送はなるべくはやくなるように距離を短くデータセンターに近くなるようにするなど工夫をする
必要なスクリプトは優先的に届けたり、必要性の低いものは別のjsファイルにしたりすることもできます。
1.1 実際にネットワークを調べる
ここでは、Networkタブで、「どのような通信があり」、「どのくらい時間がかかったか」を調べます。
重要の指標としては、Status, Size, Time, Waterfallです。
説明は不要かと思いますが、Waterfallとはリクエストのダウンロードの処理の詳細と、かかった時間のことです。
特にこのサイトだと、以下のような画像が非常に遅いことがわかります。
- 遅い画像は明らかにデータサイズが大きい
ので、データサイズを改善する必要があります。
あとから気づいたのですが、時間でソートして表示すればよかったです、すみません。
また、各々の詳細は項目を選択しTimingタブを見ることで設定できます。
TTFBは最初の1バイトがくるまでの時間を表しています。 6msとわりかし高速であり、フロントエンドエンジニアが何かできることはありません。
例えばTTFBが遅いと通信状態が悪かったり、バックエンドに問題があるのではないかと予想することができます。
サーバーサイド、インフラの経験からクラウド環境のネットワークを考えることもできるとは思いますが、、、今回の記事ではそっとしておきます。
Contents Downloadはコンテンツ(画像なりファイルなり)をダウンロードするまでの時間ですコンテンツのサイズが非常に大きいのでダウンロードに時間がかかっているのでしょう。
これは、レンダリングのところで再度指摘します。
特に、通信のオーバーロードがかかっているわけではないので、ネットワーク処理としては大丈夫そうです。
他にも改善方法としては、
- 画像の遅延ダウンロードする
- 不要なライブラリどを削除する
などができると思います。
今回は遅延ダウンロードをするためにvue-lazyload
というものを使いました。
- コンポーネットごとまるごとlazyload
- 特定のタグをlazyload
を簡単にできるので採用しましたが、かなり使いやすかったです。
1.2 通信状態の悪い状態で試してみる
通信制限や、単純に電波が届きにくい場所にいるなど通信状態が悪い時はいくらでもありえます。
そのようなときにDevToolではシミュレーションをして、どのようなUXになるのかをあらかじめ知ることができます。
試しにSlow 3G
でやってみます。
DOMContentLoadに7.63
秒で、Loadには28.48
秒なので、わりかしはやくに何かしら表示できているのではないでしょうか。
2. レンダリング処理に関して
レンダリングの原則は以下の通りです。
- 1フレーム内の処理を軽くすること。60FPS以上が理想的
- ブラウザの内部処理を利用して最適化を測ること
特に"Performance"で知ることができるので使ってみます。 Recordを押して記録をはじめ、適当にスクロールなどをしたら停止しましょう。
8秒ほど録画しました。 しかし、アニメーションが皆無なので特に問題そうなところはありません。
すこし大事な箇所を解説をします。
ここでもネットワーク処理について知ることができます。 スクロールなどをして画像が読みこまれるなど、ネットワーク処理が期待される場合に、調べることができます。
また、全体として、どのくらいスクリプト処理、ペイント処理、レンダリングに費やしているかを知ることができます。
再度、同じ説明になりますが
- スクリプト処理: Javascriptの実行によるもの
- ペイント処理: 実際にレンダリングツリーを端末内の画面のピクセルとして配置すること
- レンダリング処理: DOMツリーとCSSOMツリーを合わせること
ペイント処理で特に困ることといえば、CSSなどでbox-shadow
などの比較的に重いグラフィカルな処理のことです。一般的には問題になることはありませんがあまりにも多用しすぎると重くなるので注意が必要です。
しかし、やっぱりレンダリング処理が重いです。
2.1 ペイント処理に関して
ここだけ、副セクションを定義して調査したいと思います。
繰り返しになりますが、ペイント処理とは
ユーザーの画面に合成されるピクセルを書き込む処理
のことです。
- 要素を変更
- 背景、テキスト色、影などの非形状プロパティの変更をしたとき
などでレイアウトがトリガーされたときなどにペイント処理が発生します。
前者(要素の変更)はレイアウトをトリガーしています。
後者はレイアウトはトリガーされませんが、以下のようなパイプラインが実行されます。
レイアウト算出と同様に包括的な計測には"Performance"が参考になります。 ここで"Enable Advanced paint instrumentation"をチェックしてください。
以下が測定した結果です。
これによって具体的なCSSの何が遅いのかを知ることはできません。 適宜、遅いところを調査することになります。
3. スクリプト処理に関して
スクリプト処理の原則は以下の通りです。
- UIブロッキングをするような処理を避ける
- メモリリークを検出し、節約するようにする
javascriptはメインスレッドで実行するので、その間はレンダリング処理をすることはできないので、その間は塞がれることになります。
3.1 メモリリークを見つける
メモリリークの調査をします。 ヒープ領域の調査は"Memory"タブで計測することができます。
ヒープとは、動的に確保されるメモリ領域で、例えば以下のようにnew
するとヒープアロケートがおきます。
var date = new Date()
そして、スコープを抜けるとガーベジコレクションによってメモリ解放されます。
if (true) { var data = new Date() // ここでメモリ解放 }
しかし、何かしらの問題でメモリが解放されずに漏れるメモリリークというものがあります。
メモリリークの調査方法は以下の動画で知りました。
なぜメモリリークを解決するのが大事というとメモリリークすると
- スクリプト処理などのパフォーマンスが悪くなる
- 画面がかくつく
などいろんな問題があります。
"Memory"タブではヒープアロケーションとヒープスナップショットをとることができます。
また、重い処理は再度"Performance"で調べることができます。ヒープ(領域)とはプログラムが一時的に使用するメモリのことで、OSなどによって割り当てられ、主に関数間で変数などを使いまわせるようにするためにヒープを使います。
ここで以下のように選択します。
そして、実際に録画して、何かメモリリークしそうなところをぽちぽち押したりして、ヒープを確認します。
青い線がヒープアロケーションで、メモリが割り当てられています。
青い線がありますが、イベントごとにlistenerを解放しているので青い線はすぐ灰色の線に変わるので問題はありません。
実際にメモリリークがあれば、スナップショットを取れば解決できます。
最初にページをロードしたときにスナップショットを取ります。
そして気になるところ(タブを何回も開いたりして)再度、スナップショットをとります。
そしてSummery
ではなくComparision
を選択しましょう。
すると二つのスナップショットで比較をすることができます。
私はないのですが、赤で表示される項目は参照がなく、メモリリークをしている状態です。Swiftだとweak
など弱参照をつけて避けていたようなことが実際に起きているのでバグを直す必要があります。
一例としては、よくやる間違いですが
- Aという要素のイベントリスナーBを定義する
- Aが消される
- Bが取り残される
のケースがありえます。
実際に問題を直す
1. 画像を圧縮する
Vue.jsでは、プロダクション環境ではすでに圧縮されていますが、元の画像を圧縮するといいでしょう。
ChromeのAuditを使うとWebP
を推奨されますので、<picture>
でWebP非対応のブラウザも対応します。
WebPとはPNGより26%小さく25~34%ほどJPEGよりも小さく同等の画質の静止画フォーマットです。
対応状況は以下の通りです。
まず
brew install webp
でインストールができたあとに
cwebp input_file -o output_file.webp -q [画質]
と指定します。[画質]
はデフォルトが75のようで、小さいほど荒くなってしまいますが、画像を小さくできます。
実際にHTMLで記述するときは<picture>
で囲ってください。
<picture>
とは
- 0個以上の
<source>
- 1個以上の
<img>
を含むもので画面や端末にあった画像を提供できる要素です。
<source>
の中から適切なものがあればそれを選び、なければ<img>
を使います。
picture source(srcset="hogehoge.webp") img(src="hogehoge.png")
のように使います。
画像だけNetworkタブでみたいときはFilterでImg
を指定すると見ることができます。
元の画像の次元数を減らさなければなりませんので、すべて約50%縮小しました。
再度、AuditとPerformanceによる評価をします。 ここではモバイルで検証します。
ポイントが時間に対して線形ではないのですが、特にいいスコアを出していますね。 また、ネットワークの方は、画像群がかなりボトルネックになっていますね。
2. 遅延ロードを実装する
次にコンテンツが下のものは遅延ローディングを実装します。
例えば以下のように実装できます。
.protofolio-img img(v-lazy="'https://storage.googleapis.com/keisuke-portofolio/portofolios/' + portfolio.headerImage" alt="portfolio")
まずは画像ファイルから。パラパラ読み込まれていく様子がわかります。
今回速度を計測すると、たまに1.2秒を下回るくらいです。
同様にはてなブログの記事も、遅延ローディングしました。
案外、はてなブログのリンクも重かったためです。
ここでキャッシュを使わずに、約0.8秒を実現しました。
また、コンテンツをGoogle Cloud Storageに保存しました。 無料枠をすべて使ってしまっていたので同じファイルに保存していました。
Auditスコアはこちらです。
2. 再度、圧縮する
27インチのiMacの全画面でみてみて、許容できるまで再度圧縮しました。
なぜもう一回するかというと許容できる粗さまでに圧縮したいと思ったからですし、伸び代(高速化)の一つです。
キャッシュを使ってですが、0.2
秒くらいで表示できるようになりました。キャッシュを使わないと0.6秒くらいです。
3. 肥大化しているJSファイルを見つけて縮小させる
Webpackとは、先ほどのも説明したようにバンドルファイルにまとめてくれます。
しかし、ファイルをまとめるといっても
- まとめ方がおかしい
- 使わないものが混じっている
などいろんな可能性があります。逆に必要なものをレンポンスに含めなれなかったり、取得が遅かったり問題を起こす反面もあります。
また、まとめることによってHTTPリクエストを減らしたら、ファイルの大きさを小さくできることができ、本記事での目的である「爆速化」に貢献するのは間違いありません。
副次的なものとしてはデバッグしやすかったり、管理しやすかったりします。
例えば以下のようにdist
以下はなっています。
dist/ ├── index.html └── static ├── css │ ├── app.7f765b1ba440fdc48331e45914d9d7ed.css │ └── app.7f765b1ba440fdc48331e45914d9d7ed.css.map ├── favicon.png ├── fonts │ ├── feather.5fad700.eot │ ├── feather.66cbb62.woff │ └── feather.a940fe8.ttf ├── img │ └── feather.023ba08.svg └── js ├── app.43a20de5e475988e5126.js ├── app.43a20de5e475988e5126.js.map ├── manifest.2ae2e69a05c33dfc65f8.js ├── manifest.2ae2e69a05c33dfc65f8.js.map ├── vendor.f37bcea6cc3c0912b65c.js └── vendor.f37bcea6cc3c0912b65c.js.map
まず、以下のコマンドでWebpack Bundle Analyzer を使って可視化をして、一体何が大きいのかを探る必要があります。
npm run build --report
するとlocalhostで以下のようなページが立ち上がります。
まず、支配的な項目から縮小させていきます。
前提として、濃淡によって依存関係が階層構造になっています。
明らかにat-ui
が大きすぎます。
参考にat-ui
が何であるかというと軽量なUIライブラリです。
使用箇所は以下の自己紹介部分です。
しかしながらファイルの大きさは約330KB
で、gzipでも35KB
くらい食っているので、できるならば自分でCSSなどで直接書いて置き換えます。
試しにローカルホストで起動します。
まだ取り除いていないので以下のようになっています。
at-ui
をプロジェクトから取り除きました。
すると以下のように1.1
MBになっていて約400MB分だけ縮小しました。
再度、可視化をすると以下のようになっています。
これから支配的なのはVue自体ですが、これは私自身では改善が望めないのでここでやめました。
しかしながら、転送速度が約100
ミリ秒変わったので、大きさ一歩ではないでしょうか。
デプロイ環境では以下のようになっています。
まとめ
今ではこんな感じですね。
今回は爆速化ということでスピードにフォーカスしたのでPWAやオフラインでの閲覧などは考量してないです。
まとめとしては
- ネットワーク処理は、基本的にhttp通信に関すること
- ブログうめこみなど、ボトルネックがある可能性がある
- サーバーサイドなどの経験があれば、イベントリスナーなどくらいしかスクリプト処理は問題にならないはず
- といれど、
for
などがあれば計算量を想定すべき
- といれど、
- レンダリング処理は、特に画像を正しく用意しないとボトルネックになりがち
- 数MB単位の画像を使っているサイトもあったり、以外に対策法は多様である。
もちろん速度は大事ですが、それはUXが何より大事だからです。 1秒以上待たされるけど、ローディングアニメやプログレスバーがリッチだったら、逆に待ち時間は短く感じるかもしれません。
まだまだそっちの方面で勉強しないといけないと痛感しました。
web-grpc
などやってみたいことがたくさんありますが、卒業のために卒論に集中します。
ご覧いただき、ありがとうございました。
おまけ
Lighthouse CIの存在
あまり知られていませんがGithub CheckAPIを使ったLighthouse CIというものがあります。
公式リポジトリから参照しますがCheckAPIを使って以下のように常にCIツールのようにAudits Scoreを知ることができます。
エンジニアとしてはフィードバックを常時もらえるようになるのでぜひ設定してみてください。