Help us understand the problem. What is going on with this article?

わいのNuxt.jsアプリケーションは遅かった。

この記事は 今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎 Advent Calendar 2019の3日目の記事です。
プログラミング初級者がNuxt.jsの高速化するのにストレージにキャッシュしてみたら、色々捗った事を書きます。
わいさん風を取り入れてみました。少しなまりが間違っている箇所があるかもしれませんが、ご容赦下さい。

Nuxt.jsでSSRしたら最速なのか?

ある日の転職したくて、ポートフォリオアプリを作っているワイ。

ワイ 「Nuxt.jsでサーバーサイドレンダリングが簡単に出来るみたいやな。」
ワイ 「サーバーサイドレンダリングで、アイソーモフィックでユニバーサルなシングルページアプリケーションで作ってみました、とか面接で言えば転職間違いなしやな」
ワイ 「ヘッドレスCMSが流行ってるみたいやな。Blogアプリとかいいやん。作って自分で勉強した事をアウトプット出来るしな。(本当はどうせ、作って満足して、アウトプット出来ないBlogアプリを量産するんやけどな。。)」
ワイ 「GraphCMSってのがあるやん、GraphQLでリクエスト
すればJsonでレスポンス返してくれるヘッドレスCMSやん。これ使えば、GraphQLは使えますって見栄張れるな。」
ワイ 「FirebaseのFunctionsでSSRしたらなんかかっこいいやん!私はサーバレスですって言えるやん。」
ワイ 「Bootstrap-vue使って、GraphqlのリクエストはAppoloClient使って、FontAwesome使って、GoogleFont使って・・・・・・よし出来た!」
ワイ 「なんでもNuxt.js用にライブラリ作ってくれてるもんで、便利やな!」

ワイ 「これでわいも爆速で最強のSSRやでって言えるな。よし、LightHouseしてオール100点ですっていうのをスクショ取ってQiitaで見せびらかしたろ」

first_lighthouse.png

ワイ 「全部緑色ちゃうやん!」
ワイ 「見せびらかせやんやん!」

LightHouseのパフォーマンス

上記のスクショでは、65点でしたが、凝ったWebアプリを作れば作るほどもっと悪いスコアが出る方も多いのではないでしょうか?(私はそうでした。)
パフォーマンス以外の部分は、LightHouseの評価するポイントを埋めていけば100点に近づいていきます。足し算の努力で対応出来ます。
しかし、パフォーマンスだけは、足し算だけではスコアが上がりません。この記事では、パフォーマンスを100点に近づけていく事をテーマに個人開発でポートフォリオを作っている方が、取り入れる時に参考になりそうな事を考えてみました。

まずLightHouseについて

Lighthouse(ライトハウス)は、もともとGoogleが提供しているChrome拡張機能でしたが、現在はChromeに標準搭載されて、デベロッパーツールのAuditsから使えるので、拡張機能はインストールしなくて大丈夫です。Goolgeが考えるWebサイト、Webアプリ
(モバイルも含む)が目指す先の道しるべとしてのLighthouse(灯台)という意味です。

スコアは変動する

LightHouseのスコアは、変動をできるだけ最小限に押さえられる様に作られているそうです。
しかし、インターネット回線の速度、実行するデバイスの違い、アクセスするサイト全てに手を加えるChrome拡張を入れていたり、異なるバージョンのLightHouseで計測する等様々な理由で変動します。出来るだけ同じ環境で、複数回計測し、平均点をあげて行けば良いのかなと思います。

LightHouseのバージョンによる違い。

スコアの算出方法としては、Googleが考えるベストプラクティスなので、googleが考えるユーザー知覚パフォーマンスに影響を与える度合いというもので、これから説明する、いくつかの指標に重み付けをして算出されています。
さらにこの重み付けは、LightHouseのバージョンが変われば変わってしまいます。
つまり一度、高スコアを出しても、Googleさんの考えが変わればスコアは変動するという事です。
なのでLightHouseのスコアを高くする事を目指すのがとにかく良いということでは無いという事は理解しておく必要があります。
参考URL:https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics

Perfomanceのスコアを算出する為にどんな指標があるのか?

詳しくは、https://developers.google.com/web/tools/lighthouse に書かれています。
この記事では、ざっくりとした説明にしますので、気になった部分は、こちらを確認してみて下さい。
スクリーンショット 2019-12-02 13.14.04.png

Chrome Dev Summit 2018の画像

スクリーンショット 2019-12-02 16.46.40.png
Paul Irish at Chrome Dev Summit 2018

画像をお借りして私のざっくりとした理解ですが、

  • FirstContentfulPaintがリクエストから、クライアントからアクションを起こせる状態になるまでの時間で、
  • その状態から一番時間がかかるタイミングで一番時間がかかるアクションを取った時のアプリからのレスポンスにかかる時間がMaxPotentialFirstInputDelayである。
  • Time to Interactviはページが完全にインタラクティブになるまでの時間です。

NuxtでSSRをすれば、Htmlをサーバ側で、生成してくるので、FirstContentfulPaintは早いが、早い分、MaxPotentialFirstInputDelayは大きく出るという事になると考えられます。MaxPotentialFirstInputDelayの計測値にGoogleが重きをおけばおくほどスコアは下がっていくというように考えられます。

ざっくりスコアを上げる為に主なできる事

  • クライアントのリクエストスタートからサーバのレスポンスを早くする。アーキテクチャの見直し。

  • なるべくCSS・JS・画像データを軽くして読み込みを早くする。

  • ソースの読み込み時にブロッキングが発生している所がないか確認、あればブロッキングしないように出来ないかやってみる。
    lazyloadとか。prefetchとかPrerenderとか。

今回のわい君が思いついたアプリ

スクリーンショット 2019-12-02 14.00.52.png

FirebaseFunctionsでSSRする時は要注意

FirebaseFunctionsをFirebaseHostingをドメインを結びつけて使用する際には現在、
リージョンはUSセントラルしか使えません!

つまり、クライアントが日本にあるとすると、USセントラルのFirebaseにリクエストを送ると、東京リージョンのGraphCMSにデータを取りに行き、USセントラルでから日本にレスポンスを返してきます。

世界1周旅行です。

GraphCMSのリージョンをUSにすればいいんじゃ無いかと思った方もいるかと思います。

しかし、CMSをUSに持っていっても、今度は、CMSで管理している画像を保存しているストレージがUSに行ってしまいます。

初期表示時にデータを入れてSSRをしたい時には、SSRを行うアプリケーションの核となる部分はクライアントに近くないと考えることが多くなると感じました。

世にあるCMSや、情報を発信してくれるAPIや、画像を圧縮してくれるAPIをアプリケーションに取り込む際は、リアルタイムにクライアントのレスポンスに応じて、APIサービスを使用するのでは無く、あらかじめストレージにキャッシュしておけるならしておくというような使い方をしなくちゃなと思いました。

CMSのデータをストレージにキャッシュさせるといい事いっぱい

スクリーンショット 2019-12-02 14.51.06.png

CMSで新しい記事を書いた時、または編集した時にフックしてFirebaseFunctionsをから、GraphCMSからデータを取り、ストレージに保存します。

すると、クライアントがBlogページへリクエストを送った時には、データはストレージに取りに行きます。先ほどのアメリカ日本を往復するデータ取得の処理は、クライアントがBlogを閲覧する時には走らなくなりました。

また、CMSへリクエストを投げる為に使っていた、ApolloClientもFunctionで、使うのでNuxtには必要なくなりました。少しデータが軽くなりました。

さらに勝手に気を利かせてGCPがCDNにキャッシュしてくれているみたい!

クライアントで欲しいデータを外部APIへ取りに行ったり、そのデータを加工したりするのは、FaaS,PaaS等にデプロイした自家製APIに任せて、使いたいデータの形式にしてストレージにキャッシュしておくのがいいなと思いました。

Blogアプリで考えられるのは、Markdown形式で書かれたコンテンツデータをHtmlにパースする事をストレージにキャッシュする前に処理出来ないか等が考えられると思います。まだ出来ていませんが、これは何を作る時でも色々考えられる事だと思いました。

pwa-moduleは入れる

Nuxt.jsは簡単にPWAを実現出来ます。簡単すぎて、serviceworkerについては別途学習しないと何も理解出来ないくらいです。この導入で、PWAモジュールに含まれるworkboxモジュールによってサービスワーカーが導入され、キャッシュが効くようになります。 Googleは現在、PWA推しのようです。

$ yarn add @nuxtjs/pwa
nuxt.config.js
modules: [
    '@nuxtjs/pwa
]

とにかく軽くする

$ nuxt build --analyze

しまくります。削減可能な部分を考えたり、重いライブラリを軽量ライブラリに置き換えたりします。

難しいことは考えずにとにかく軽くする方法を考えました。

FontAwesomeは使うアイコンだけ読み込むようにする。

FontAwesomeの導入方法は様々ありますが、いっぺんに全てのアイコンを読み込んでしまうものがあります。
よく使われているものでも、@fortawesome,nuxt-fontawesome,@nuxt/font-awesome,vue-fontawesome,vue-awesomeとか他にもあるかもしれません。
初級者向けの記事を参考にすると、出来るだけ簡単に単手順で全て取り込めて、どのページでも全てのフリーのアイコンを使えるようなものを採用することになってしまいます。

私は、軽量化するに当たって、@fortawesome/vue-fontawesomeを使ってプラグインで使うものんだけ読み込んでみました。これでも大きな軽量化になりました。使用するコンポーネントだけで読み込んだほうが良いのか、vue-fontawesome以外も使ってサイズの違いを確認してみます。

nuxt.config.js で プラグイン で読み込む

nuxt.config.js
css: [
       '@fortawesome/fontawesome-svg-core/styles.css'
     ],
plugins: [
    {
      src: '~plugins/font-awesome',
      ssr: false
    },
     ],

使用するものだけ読み込む

plugins/fontawesome.js
import Vue from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faCalendarAlt, faTags } from '@fortawesome/free-solid-svg-icons'

library.add(faCalendarAlt, faTags)

Vue.component('fa-icon', FontAwesomeIcon)

Vue.config.productionTip = false

コンポーネントライブラリは重い

今回、CSSフレームワークとして、使ったのはBootstrap-vueでした。BootStrap-vueは、コンポーネントのライブラリとなっています。CSSだけでなくJavascriptも内部で使っています。これが使ってない部分も全て取り込まれてしまいます。自分でJSを書く必要はありますが、ここをCSSのみのライブラリに変更出来れば軽量化出来ます。
スクリーンショット 2019-12-02 16.02.08.png

まだやりたい事は全部できてませんが、

まだ、bootstrap-vueは置き換えていません。
スクリーンショット 2019-12-03 0.11.36.png
さらにGraphCMSでは記事の内容を更新したり、新規投稿したりした時にHookして何かアクションを起こしたりする事は無料では出来ないことが発覚しました。とりあえずの間に合わせで、CMSを更新したら、ターミナルからrefreshコマンドを打てばリフレッシュ出来るようにしました。ポンコツ仕様ですが、自分でCMSを作りたいというモチベーションが出来たので無駄では無いと言い聞かせます。

結果、90点以上は平均して出るようになりました。

スクリーンショット 2019-10-04 17.07.18.png

ワイ「これで緑色にはなったな」
ワイ「データ駆動やからこそ、もらうデータのパターンが決まっていてキャッシュしやすいものはキャッシュ最強やな」
ワイ「それにしてもLightHouseを使って$ nuxt build --analyze叩いているうちに重いライブラリをみると敵に見えてきてしまうようになってしもたで。」

まとめ

ブログサイトやポートフォリオページなんかの更新がそれほどリアルタイム性が必要無いものは、ストレージにキャッシュする事でいい事満載だなと思いました。フロントエンドの役割が大きくなっていくにつれて、Firebaseにバックエンドは任せて、初級者はなんでもライブラリをフロントエンドに詰め込みまくってしまいがちです。
とりあえず、FirebaseStoreのデータを全部読み込んで、複雑なデータ処理もフロントエンドで行なったりしがちです。

私がその典型でした。

しかし、出来るだけサーバーサイドにデータ処理のロジックを回したり、外部APIをから手に入れるデータがある際は、キャッシュしたりする事でフロントエンドが軽量に出来るという視点を持たなくてはならないなと感じました。

今回の内容には直接関係無いかもしれませんが、最終的な成果物です。
ポートフォリオ的Blogアプリですが、作って満足して、内容は更新できていません。汗

最終的なBlogサイトのデモページ:https://nuxt-deploy-test-teshima.appspot.com/
コード:https://github.com/yujiteshima/nuxt_blog_LT

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away