JavaScript/Vue.jsアプリケーションのパフォーマンスチューニング

宇都宮諒氏:では、Vue.jsアプリケーションのパフォーマンスチューニングの話をしたいと思います。

自己紹介ですが、株式会社一休の宇都宮と申します。宿泊事業本部のUIチームに所属していて、主な仕事は宿泊予約サービスのUI開発と、『一休コンシェルジュ』という自社メディアの開発サポートをしています。

今日のアジェンダです。

JavaScriptやVue.jsアプリケーションのパフォーマンス改善の話をする前に、まず一休でどのようなパフォーマンス改善をしたか。あとはフロントエンドのパフォーマンス改善に、そもそもどういうことが必要なのか。そのあたりのお話をしてから、各論としてJSやVue.jsの話をしていきたいと思います。よろしくお願いします。

まず「一休.comパフォーマンス改善プロジェクト」を2018年の4月くらいからやっていて、エンジニア3名で3ヶ月ほどかけて実施しました。対象はスマートフォンのホテルの詳細ページです。

ここは、以前から「ちょっと遅い」という声が上がっていました。パフォーマンスチューニングはまったくしていないわけではなくて、とくにサーバーサイドはいろいろなことをやっていましたが、フロントエンドが足を引っ張っていたという事情がありました。

フロントエンドの大幅な書き直しと、それに伴ってサーバーサイドにも「いろいろ改善したいよね」という声があったので、そこを改善しました。

具体的にどんな課題があったかというと、そもそもサーバーのレスポンスが遅い。それから、これは古いサイトにありがちですが、レンダリングをブロックするJavaScriptがありました。あとはそもそもJSのサイズが大きかったり、CSSがすごく巨大なSassになっていて、メンテがつらいとか、画像も本来の表示領域に対して過剰に大きな画像を読み込んでいたり、そんな課題がありました。

パフォーマンスを改善するためにやったこと

それに対して何をやったかというと、一番大きなところとしては、サーバーサイドのフレームワークを差し替えました。

ASP.NETのWebFormsというフレームワークからMVCというフレームワークに移行しました。.NETは非同期処理を簡単に行える言語なんですが、Web Formsは非同期処理を上手く扱えませんでした。MVCによって非同期処理をフル活用できるようになり、Solrの検索やDB問い合わせなどの処理を並列化した結果、Time To First Byteが平均200msくらいのパフォーマンスが出るようになりました。

あとはフロントエンドもほとんどゼロベースで書き直しをして、パフォーマンス改善を行いました。imgixを使った画像最適化も行いました。ただデザインは変えないで、できるだけスコープを広げすぎないように気を付けてプロジェクトを進めました。

どのように改善されたかといいますと、ちょっと画像が荒いですが、ChromeのディベロッパーツールのAuditsの中で使える、Lighthouseというパフォーマンス計測のツールを使って計測した結果です。

Beforeは21点で、これはけっこう遅いサイトです。Afterでは65点。これはスコア的には平均くらいなんですが、ほかの宿泊予約サイトの同等のページと比べて遅いかというと、そういうわけでもなくて。

当時、競合の中で1番速かったのがBooking.comのホテルページで、これを目標にチューニングしてきましたが、一休のほうが速いくらいのパフォーマンスが実現できたので、結果的には成功だったかなと思っています。

あと、この計測は、Simulated Fast 3Gというプロファイルでやっています。回線速度的には1.4Mbpsで、お昼時の格安SIMの通信速度と同じくらいの状況です。

Webフロントエンドのパフォーマンス最適化

ここからは、Webフロントエンドについて、今までお話してきたような改善をするためにどんなことをやる必要があったかをお話します。

まず、パフォーマンス改善では、測って、改善して、改善した状態をずっと維持する必要があります。これは、身近な活動としては「ダイエット」に似ています。ちゃんとサイクルを回して、なおかつ維持をしないと、すぐに遅くなってしまいます。

最初は計測から始めます。

計測に関しては、フロントエンドはLighthouseというツールがデファクトスタンダードになってきてるかと思ってます。今だとChromeDevToolsにもPageSpeed Insightsにも、ほかにもいろいろなSaaSにLighthouseが入っています。

Lighthouseの使い方の注意点なんですが、必ずThrottlingをしないと、ローカルの回線速度によって計測結果が変わってしまいます。

あとは回線速度もちゃんと絞って、Fast 3Gなどで計測しないとモバイルのパフォーマンス計測としては不適切な結果になってしまいます。

日本だと、みんな4Gを使っているという状況はたしかにありますが、先ほどお話したように、お昼時の格安SIMでは1Mbpsくらいしか出ないこともあります。しかもお昼時がピークタイムだったりするので、一番ユーザーが使ってくれる時間帯に快適に使ってもらえるようにするには、Fast 3G、1.4Mbpsくらいの設定で快適に使えるようにサイトを作る必要があります。

計測環境は、ローカルだとCPUの状況によって結果が変わってしまうので、できればSaaSを使って計測環境を揃えるほうが良いと思います。

ページ速度をPageSpeed Insightsで測定

ここからは具体的に、PageSpeed Insightsの結果を使って説明していきます。2018年の11月からPageSpeed InsightsがLighthouseを使うようになって、それでだいぶスコアが変わっています。昔のPageSpeed Insightsはチェックリストに基づいた減点方式で、90点以上を取るのはそれほど難しくありませんでしたが、現在のPageSpeed Insightsは実際のページの速度を計測していて、90点以上をとるのはかなり難しいです。実際に色々なサイトを触ってみた実感としては、50点以上取れているサイトは、それほど遅く感じないかなと思います。

Lighthouseのすごくいいところとして、改善できる項目を診断して、「こういうところ直したほうがいいよ」と教えてくれます。

これを使うことで、何を直したらいいのかという次のヒントにつなげられますので、かなり有用なツールだと思います。

計測して「なんか遅い」というのはわかるかもしれませんが、そこからどれくらい改善すればいいのかはわからないと思います。そのときに1つの参考になるのがRAILモデルです。これはロードは1秒以内とか、レスポンスは100ミリ秒以内に返しましょうとか、そうした目標値を決めたものです。ただ、これは比較的厳しいので、すぐに実現するのは難しいと思います。

競合のサイトと速度を比較する

RAILモデルの達成が難しいと感じる場合、自分たちのサイトと似ているビジネスや似ている構成のサイトを計測してみるのが、目標設定においては有効かなと思います。

一休.comの場合、競合にはBooking.comなどさまざまなサイトがあります。それらの競合サイトのページを計測してみたところ、1番速かったのがBooking.comでした。

なのでBooking.comをターゲットにして改善し、結果的にホテルページに関しては、Booking.comよりも速くすることができました。今20点のサイトが90点を目指すのは現実的ではないかもしれないですが、50点くらいのサイトを目標にして改善するならけっこういけるんじゃないかなと思います。

あとは、そもそも競合より速いかどうかもページスピードの1つのポイントになると思うので、この方法は有効かなと思ってます。

逆にほかのサイトを計測するときに、例えば爆速で有名なdev.toとか。そうしたサイトを対象にしてしまうと、そもそもサイトの性質が違っているので比較対象に向かなかったりします。サイトの仕様によって速くしやすいかどうかはかなり変わってきます。

パーソナライズされたコンテンツがないならCDNで結果をキャッシュしちゃえばいいですし、広告が入ってるとどうしても、サードパーティスクリプトに引っ張られてロードが遅くなったりなど、そうした難しさがあります。

あとは、計測も1回して終わりではなく、定期的に計測するのが重要です。

これは実際に一休で使っているCalibreというSaaSで先ほどのホテル詳細ページの画面を計測した結果です。だいたい半年くらいのスケールで表示しています。こういう定期的な計測をすることで時系列の流れがわかりますし、もう1つわかることとして、青いグラフがPC版なんですが、正直PC版はまだ最適化が十分ではありません。そうした遅いサイトは計測にブレが大きいということもわかったりするので、定期的な計測は有用です。

Calibreでやっていることとして、もう1つ、Performance Budgetを設定して、それを超過したらアラートがSlackに飛んでくるようにしています。

例えば、転送されたJavaScriptが300キロバイトを超えたらSlackにアラートが飛んできて、Performance Budgetを超えたから直さないといけないとか、そういう感じですね。

決めた体重をオーバーしたときにアラートが飛んでくるとか、そういう仕組みです。こういったこともできるので、なにかしらのSaaSを入れるのはパフォーマンス計測において比較的便利かなと思ってます。

JavaScript/Vue.jsアプリのパフォーマンス改善

次にJSやVue.jsなど各論についてお話します。これは今年のChrome Dev Summitのあるトークのスライドから引用した画像です。

フロントエンドは画像とWebフォントとJavaScript、この3つを改善すれば、それで速くなります。画像とWebフォントは今回お話ししませんが、JavaScriptの最適化について難しいポイントがいろいろありました。そもそもアプリの書き換えが必要だったり、仕様レベルから見直しが必要だったりなので、画像やWebフォントに比べると費用対効果がよくない性質はあるかなと思ってます。

ではどんなJSが速いJSと呼べるのかというと、より少ないJSが速いJSです。

当たり前の話ですが、Webページを表示するときにどのくらいのJSを読み込んでいるかがすごく重要です。document.writeを使っていたり同期のXHRを使っているなど、そういう明らかに遅いAPIを使っている場合はともかく、ちゃんと書かれたJSの場合は、単純にサイズがどれだけあるかがスピードの分かれ目になります。

モバイルの場合は、ページのスピードに対して最も支配的な要因は回線速度です。サイズが大きくなるとそれだけ転送が遅くなって、全体のインタラクションが遅くなります。

どれくらいのJSのサイズを目指せばいいのかというと、これはGoogleのエンジニアの人がブログで書いてる数字なんですが、3Gのモバイルだと、170キロバイト。ページで読み込むJSの合計が170キロバイトです。minifyとgzipなどの圧縮後の数字ではありますが、正直この数字を満たしているサイトはそれほどないと思っています。

Internet Archiveで見られる、モバイルサイトで読み込んでるJSの平均が、たしか350キロバイトなので、170キロバイトはけっこうきついです。一休は現在300キロバイト程度なので、もうちょっとがんばって250くらいに減らしたいという状況です。なかなか170キロバイトを目指すのは難しいな、というのが今の実感です。まずは今どれだけ読み込んでいて、どれくらいまで減らせそうか、そういったことを考えながら開発する必要があると思います。

速いJavaScriptのための設計

速いJavaScriptを書くために、どういうところに気を付ければいいのか。まず1つ、そもそもJSを使う必要があるかどうかを考える必要があります。

例えば僕が開発のサポートをしている一休コンシェルジュの話です。一休コンシェルジュはLighthouseのスコアが、だいたいモバイルで87くらい出ています。この87という数字がどのくらいかというと、dev.toとだいたい同じくらいです。

ただ、一休コンシェルジュはモノとしてはすごく単純なWordPressでできています。なぜそんなパフォーマンスが出ているかというと、ちゃんとCDNのキャッシュを効かせたり画像を最適化したりなど、そういったところを普通にやっていれば、裏側がすごく平凡なWordPressでもちゃんとパフォーマンスを出すことができます。

たとえば、画面遷移を速くするという目的であれば、SPAにするよりも、複数のページがある普通のWebサイトにして、各ページをCDNでキャッシュしたほうがシンプルに作れる、といった状況はあると思います。

もう1つがライブラリの選定です。小さいライブラリを使ったほうがパフォーマンス的には有利ですね、ということとあともう1つ。これは一休の中でも使っていますが、そもそも自分でJSを書くのではなく、チューニングされた既に速いUIコンポーネントを使う。これ具体的にはAMPの話です。

JavaScript最適化のポイント

一休の宿泊サイトについて、一方は僕たちが今年チューニングしたページで、もう一方は同じコンテンツのAMP版です。がんばってチューニングして59点に対して、AMP版もチューニングしていますが75点ほどなので、AMPを使うとできないことが増えますが、その分スピードが得られます。

今のAMPではできないこと、例えばfirst-party Cookieに触れるなど、そういうことがAMPでできるようになるとAMP版だけ用意するという状況に将来的にはなるかもしれないと思っています。

あとはJSの最適化ポイントです。

いろいろありますが、僕はロードの最適化が1番重要かなと思っています。そのためには、JSのサイズを減らす必要があります。JSのサイズを減らすために、僕らが何をやったかお話しします。

僕らの場合、ビルドにはWebpackを使っています。そのときに、できるだけ1画面で読み込むJSのサイズを減らすために、まずCode Splittingでバンドルが大きかったものを細かく分けました。

あとは、PolyfillもIE11などに対応していると大きくなってしまうので、これをできるだけ減らす方針を考えました。僕らがやっているのは、PCとモバイルでバンドルを分けるという方法です。今試しているのはPolyfill.ioというFinancial Timesが使っているCDNを使って、最新のブラウザであればほとんどPolyfillなしで配信するとか。そういうことをやりたいなと思ってます。

あと、地味に効くのがビルドツールです。とくにBabelでは6から7に上がったときにパフォーマンスの最適化が入ったので、Babel7に上げるだけで5パーセントサイズが減ったりしました。なので、ビルドツールはできるだけ最新版を使うようにしたほうがよいです

まだ導入できていない検討中のものとしては、precacheです。

Service WorkerでJSなどをあらかじめキャッシュしておきます。そうすれば、別の画面に遷移したときにはすでにブラウザのキャッシュに乗っているので、すぐにJSが読み込まれます。具体的にはWorkboxというライブラリがあるので、これを入れてprecacheできないかと検討中です。

Vue.jsのパフォーマンス

最後に、Vue.jsのお話です。まず、Vue.js自体は十分に速い。なおかつサイズもほかのフレームワークやライブラリと比べてそれほど大きいわけではなくて、むしろreact-domよりも小さいくらい。なのでフレームワーク本体のパフォーマンスは良いと思っています。

ですが1つだけ注意が必要で、Vueコンポーネントはそれほど小さくないというのがあります。

小さなコンポーネントでも1個あたり1キロバイトくらいになります。1つ1つは大したことないんですが、細かいコンポーネントを大量に作ってしまうと、例えばアイコン1つにつきコンポーネント1個というような設計をしてしまうと、アイコンの数だけサイズが膨らんでしまうため、ここだけ注意が必要です。

Vueコンポーネントの数は少ないほうが、サイズが小さくなって速いのは間違いありません。

ただ、大きいコンポーネントは保守性がよくないので、そのあたりはトレードオフで考えてやる必要があります。

あとVue.jsを使っていて、個人的にパフォーマンス最適化のポイントだと思っているのが、条件付きレンダリングです。

v-ifとv-showという2つのディレクティブがあるんですが、この中でどちらを使うべきかというと、基本的にはv-ifだと思います。

何が違うかというと、v-ifが付いているコンポーネントではv-ifのフラグがfalseのときにはコンポーネントが描画されない、そもそもコンポーネントのJSがまったく実行されない、という状態になります。それに対してv-showでfalseだと、コンポーネントのコード自体は実行されて、その上でdisplay:noneで見えない状態になっています。

なので、初期ロード時のコストで考えるとv-ifのほうが、とくにデフォルトが非表示の要素はv-ifが有利です。逆にv-showは、最初に表示しているものをあとから非表示にする場合、表示/非表示を切り替えたときにレンダリングが不要なのでこちらのほうが有利です。使い分けが面倒であれば、とりあえずv-ifを使っておけば問題ないかなと思います。

非同期のコンポーネントについて

これが最後のスライドです。

短く書いていますがいろいろな話があります。まずimportには2種類あって、静的なimportと、動的なDynamic importがあります。普段は普通の静的なimportを使ってると思いますが、動的なimportを使うと、JavaScriptのリソースを動的に、オンデマンドで取得することができます。

例えば、Vue.jsは非常に簡単に非同期コンポーネントが使えるようになっているので、これでコンポーネントを読み込む、importする関数を定義して、それをVueのcomponentsに入れておくと、そのコンポーネントが本当に必要になったときにimportされます。

例えば、v-if=”show”とやっていて、ここでtrueで切り替えてるんですが、このshowがデフォルトでfalseになっていた場合、a-componentというコンポーネントは、デフォルトではそもそも読み込み自体がされません。フラグがtrueになったときに初めてimportが実行されて、JSがダウンロード、実行されて、このコンポーネントが実行されるという流れになります。

要するに、Vueコンポーネントのlazyloadができる機能ということで、先ほどのv-ifとあわせて、パフォーマンス的にかなり重要なところだと思っています。

例えば一休のサービスには、プラン詳細の情報を表示するモーダルがあります。ここは巨大なコンポーネントになっていて、20キロバイトほどあります。20キロバイトというとVue.js1個分くらいのサイズですね。デフォルトでは読み込みたくないけれど、ユーザーが希望したときには表示されるようにしたい。そんなときにこの非同期コンポーネントを使うと、初期ロードでは読み込まないけれどユーザーがなにかしたら読み込む、というローディングが実現できます。

パフォーマンスを改善するために大切なこと

最後にまとめです。Webフロントエンドの最適化のためには、先ほどお話したように、計測して改善をして、維持をして、Performance Budgetを設定して、遅くなっていないことを確認していく作業が必要になります。

こういうことをやるときに何が必要かというと、細かいチューニングももちろんありますが、例えばフレームワークを差し替えるレベルのアーキテクチャ変更も必要になったりします。

なので、パフォーマンス改善するには、エンジニアの技術力ももちろん必要ですがそれだけではなく、サイトのパフォーマンスを上げるため、ユーザーにもっと快適に使ってもらうためには工数を割いてもいいという、サービスを開発・運営する組織のユーザーファーストな考え方が重要です。

以上です、ご清聴ありがとうございました。

(会場拍手)