GAミント至上主義

Web Monomaniacal Developer.

Nuxt3とNestJSで表示高速化&Google Search Console警告対応

求人サイト「シニアジョブ」をNuxt3, NestJSでゼロから開発&運用中ですが、Google Search Console(以下サチコ)のウェブに関する主な指標で「不良」、もしくは「改善が必要」の項目が出てくるようになりました。

seniorjob.jp

それぞれ詳しく書くと長くなって書き終わらないので箇条書き程度にやったことをメモ。

いろいろやった結果、PageSpeed Insights (以下PSI) で2023/4/21時点でパフォーマンスの点数は

PCは87点前後

モバイルは50点前後

となりました。

モバイルはかなり厳しいので個人的には黄色の50(50未満は赤)で及第点と納得してます。

数字だけでなく、主要なページは体感でもかなり早くなったのでさわってほしいところ。

まずはPSIの直接的でわかりやすい指摘から潰していきます。

Nuxt側

だいたいファイルサイズ小さくしてね系

画像ファイル

  • width, heightを付ける(styleで変えてる場合はなるべく実際に表示されるサイズに合わせる)
  • デザインで使っている画像ファイルサイズを小さくする (WEBツールで小さくしたり、webpにしたり)

求人画像もできればやりたいけど、未対応

HTML

  • 余計なタグ、classの削除
  • v-ifしかないspan, divをtemplateにしてDOM要素数を減らす(空になったりするとバグる場合もあり)
  • ループ内の直書きSVGタグをファイル化(useなんてあるんだ・・・)

/_nuxt/entry.*.js を小さくする

NuxtでHTMLのあと、一番最初に読み込まれる外部JSファイルです。ビルド時に生成され*にはハッシュ値が入ります。

NuxtやVue Router系、plugins、middlewereなどが含まれます。

pluginで大きめなパッケージなどを使ってると大きくなりがち。

具体的なパッケージはnuxi analyzeで検討をつけます。

nuxi analyze · Nuxt

こんな感じで一部でしか使ってないクソデカライブラリがentryに入ってしまったりしてるのを気づけるので、importする場所やタイミングを工夫したり、なるべくpluginsからcomposablesに処理を移し、必要最低限しかentryに入らないようにします。

nuxt.com

特にFirebaseのライブラリが大きかったですが、全体で使うためpluginに残しておきたい。dynamic importすることでentry.*.jsに含まれなくなりました。

/_nuxt/entry.*.css を小さくする

jsと同じくnuxt.config.cssで指定したCSSファイル, Tailwind CSSの生成物などがここに入ってしまいます。

デザインを外注したため、1つの大きないわゆるstyle.cssが存在してました。

ページによって使うものが変わるため、分割し、nuxt.configからは削除、pageやlayoutで読み込むようにしました。

Nuxt Tailwindを使ってますが、これは全ページで使うTailwindのclassをまとめて入れてしまい、大きくなっているのが課題として残ります。

理想としてはすべてコンポーネントにscopedとして書き、分割、結合はNuxtに任せることですが、共通CSSもないと辛いので難しいところ。

Tailwindプラグイン Tailwind Elements削除

アコーディオンなどで一部使っていたんですが、Tailwindのプラグインということで分割importはできず、 使っていない全部のCSS, JSが読み込まれてしまい非常にファイルサイズが大きかった。

https://tailwind-elements.com/

アコーディオンなどは自前で実装し、削除

CLS (Cumulative Layout Shift) 読み込み中のボタンなどのズレ

CLSについてはこちら

ClientOnlyにfalkbackを設定する

<ClientOnly> · Nuxt Components

サーバー側とブラウザ側でDOMが違うと起きるVueのWarn Hydration node mismatch 対策として雑にClientOnlyを使ってましたが、大きな表示ずれの原因になるのでfallbackを指定し、ずれないようにしました

たとえばログイン状態↓こうすることで「非ログイン中に出すやつ」がSSRされるようになります。

<ClientOnly>
  <div v-if="isLogin">ログイン中にだすやつ</div>
  <div v-else>非ログイン中に出すやつ</div>
  <template #fallback>
    <div>非ログイン中に出すやつ</div>
  </template>
</ClientOnly>

ログイン有無はPSIでは指摘してくれないので、自分で気づく必要がありました。

layoutをやめる

詳し動きは下記ツイートなんですが、Nuxt本体のバグが仕様か、layoutsを使うとサイト内遷移のCSR時にどうしても一瞬slot部分が空になってしまいました。

  • 2023/6/15追記 page内でuseAsyncDataなどをawaitし、navigationをブロックするのが原因。awaitをとりスケルトンローディングを表示するなどで対応可能でした

フッターの位置が大きくずれて、減点MAX(1.0)の原因になってました。

ひとまず主要なページはpage側に移して回避。

こちらのページ遷移も単一しかみてくれないPSIは指摘してくれません

いい対策が思いつかないのでlayoutでヘッダー・フッターを入れるのをやめ、改善を確認。 のちに読み込むCSSだけ切り替えるようにlayoutは使用するようにしました。

Google Tag Manager

Google Analyticsの他、いろいろ広告やコンバージョン測定系をGTMで挿入してます。

Nuxtで読み込むJSより先に動くと、処理がブロックされてページ表示速度に影響がでてPSIでも下記警告が出ます。

ビジネス的に挿入しないわけにはいかないので、ページビュートリガーをドキュメントにあるうち一番遅い「ウィンドウの読み込み」に変更し、多少マシになりました。

ページビュー トリガー - タグ マネージャー ヘルプ

NestJS

重い処理はキャッシュ

都道府県、市区町村やら職種などのいわゆるマスターデータは毎回DBから取らずにメモリにキャッシュするようにしました。

サーバーコストを抑えるためもあり、まだRedisも入れてない理由もあって、メモリキャッシュを使ってますが、ネットワークのオーバーヘッドもないし、JSONなどにシリアライズせずにJSのオブジェクト等を持てる利点もあり、今後も用途に合わせて使っていく予定。

マネージドRedisが高い & 必要なサーバー数がまだ少ない & Cloud Runが安すぎて、キャッシュ用にメモリ1GB増やしても全然安い。

求人一覧ではDBを使わずにElasticsearchだけで必要な情報を返す

サイトので一番重要でもある求人一覧ページは、Elasticsearchで条件にあう求人を検索しIDを取得、そのIDを使ってDBから詳細取得の2段階をやっていましたが、 Elasticsearchに一覧で必要な情報だけいれたフィールドを追加し、sourceから取得することで、DBアクセスを不要にしました。

レスポンスをgzip圧縮

選択肢データなどテキストなので圧縮することで大きく通信量を減らせます。 後述するCloud CDNを通すのも可能ですが、NestJSのドキュメントどおりの設定で簡単にでき今のところ負荷も問題なさそうなので追加。

Compression | NestJS - A progressive Node.js framework

GraphQLはPOSTなのでCDNキャッシュがやりづらいのもあり。Nuxt側でキャッシュするのもありな気もしてる。

クラウド側 (GCP)

Cloud CDNで静的ファイルを圧縮して配信する

Nuxt側のビルド時にgzip生成も試しましたが、ビルド時間が長くなるなど支障もあったので、こちらを利用。 ポチポチだけでできました。

動的圧縮を有効にする  |  Cloud CDN  |  Google Cloud

非同期読み込み+スケルト

CLSを防ぎつつ、ページ表示速度を上げるため、ファーストビューに入らず、取得にも時間がかかるようなコンテンツはサーバー側で処理させず、読込前後で高さが変わらないようにスケルトンローディングを表示するようにしました

サイト内リンク系はSEO的にSSRさせたいけど、結構重い処理もあったりして悩みどころ。