Nuxt静的PCページでPageSpeed Insights高得点を狙う話

こんにちは、新卒フロントエンドエンジニアの古園です。 普段はクライアントワーク事業部の案件でNuxt.jsを扱っています。 今回はそのNuxt.js環境でPageSpeed Insights高得点を目指した議事録的なものを書き記します。

事の発端

受け持ったとある案件にて・・・

クライアント「PageSpeed Insightsの点数低すぎるんだけどもうちょいなんとかなりません?」
私「確かに(6点)」

というわけで、改善することになりました。

検証開始

改善するのはいいものの、そもそもNuxt.jsのようなフレームワークでパフォーマンス高得点を目指せるのか疑問がある。まずは何も弄っていない、create nuxt appして作っただけのプロジェクトで高得点を狙えるのか試してみよう。

npx create-nuxt-appの設定はこちら

? Project name: nuxt-high-score
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Buefy
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)Axios, Progressive Web App (PWA), Content
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)ESLint, Prettier, Lint staged files, StyleLint, Commitlint
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)jsconfig.json (Recommended for VS Code if you're not using typescript), Semantic Pull Requests, Dependabot (For auto-updating dependencies, GitHub only)
? Continuous integration: GitHub Actions (GitHub only)
? What is your GitHub username? miyabin66
? Version control system: Git

これだけではWeb上で確認できないのでfirebase initでFirebase環境も用意。(その他Firebase関連の操作は今回の趣旨とはあまり関係がないのでカット)

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites
? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? No
? Set up automatic builds and deploys with GitHub? No

上記の操作と一応yarn outdatedでmoduleを最新化したらFirebaseに上がったページの点数を確認。

f:id:kuroda9029:20201121205255p:plain

パソコン(以下PC)は100点。初手から良い感じ。

ただし、改善出来る項目と診断にそこそこの数の表示があるため、このまま開発を行っていくと確実に点数は下がっていきそうな予感はする。

試したこと

まずは、nuxt-compressを使って安定の(?)ファイルの圧縮から取り掛かる。

  1. インストール
    yarn add -D nuxt-compress

  2. modulesに追加

modules: [
    ...,
    [
      'nuxt-compress',
      {
        gzip: {
          cache: true,
        },
        brotli: {
          threshold: 10240,
        },
      },
    ],
],

ついでにnuxt-optimized-imagesで画像の圧縮もできるようにした。

  1. インストール
    yarn add -D @aceforth/nuxt-optimized-images

  2. 圧縮に必要なモジュールをインストール(ここでは敢えてリファレンスに書いてある対応出来るもの全てを入れた。本来は必要な物のみインストールする。)
    yarn add -D imagemin-mozjpeg imagemin-pngquant imagemin-gifsicle imagemin-svgo webp-loader lqip-loader responsive-loader sqip-loader sharp

  3. nuxt.config.jsに設定

modules: [
  ...,
   '@aceforth/nuxt-optimized-images',
],

optimizedImages: {
  optimizeImages: true
}

これでFirebaseに上げて確認してみたら点数が96に下がる結果に。何故だ。

追加調査

点数が下がってしまったが圧縮自体に効果が無い筈がないのは流石に分かる。 そこでとりあえず他の項目から対処することにした。

  1. まずはウェブフォント読み込み中のテキストの表示から行う。
    恐らく原因はBuefyが読み込み終わるまで文字フォントが設定されず何も表示されない時間があるからな模様。
    default.vueにfont-familyを設定してみたが、そもそもウェブフォントをBuefyで読み込んでるようなので解決は出来なかった。
    どうしても対処するならBuefyのアンインストールしか無さそう。

  2. 続いて、静的なアセットと効率的なキャッシュ ポリシーの配信。とりあえず点数向上ができればいいので1年間の設定でfirebase.jsonに書き足し。

"hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
// 以下追加
    "headers": [
      {
        "source": "**/*.@(js)",
        "headers": [{
          "key": "Cache-Control",
          "value": "public, max-age=31536000000, immutable"
        }]
      },
      {
        "source": "**/*.@(css)",
        "headers": [{
          "key": "Cache-Control",
          "value": "public, max-age=31536000000, immutable"
        }]
      },
      {
        "source": "**/*.@(png)",
        "headers": [{
          "key": "Cache-Control",
         "value": "public, max-age=31536000000, immutable"
        }]
      }
    ]
}

これをやると100点に帰って来ることができた。相変わらず解決できなかったフォントの警告は存在している状態なので、Buefyのようなフレームワーク&モジュールを入れた状態でも満点にするのは不可能では無いらしい。

f:id:kuroda9029:20201128224016p:plain

本題は難解

以上の調査&検証を以って案件の改修に取り掛かろうとしたのだが、

先輩エンジニア「客先サーバーにキャッシュ設定出来る訳が無いよね」

私「ですよねー」

先輩エンジニア「ついでに調べたらgzip対応してなかったね」

私「...はい」

そう簡単にはいかなかった(知ってた)

再挑戦

という訳で地道に現在のコードを改善していくしかなくなった。
ひとまずお決まりのyarn build --analyzeでファイルの状態を確認したのだが、どうもapp.jsやindex.jsが肥大化している模様。

解決策

結論から言うと1番効果があったのは先輩エンジニアが発見した複数ページで使い回すコンポーネントで画像を読み込むと何故か全てのページの画像がindex.jsに紐付けされる状態の回避。具体的に解説すると画像読み込みのコンポーネントを作成し、以下のような形で使い回していた。

ページ:

<template lang='pug'>
  ...
  CommonImages(:src='I/know/what/that/Saitamese/needs.png' :alt='Feed it some weeds!')
</template>

コンポーネント:

<template lang='pug'>
  img(:src=`require(${src})` :alt='alt')
</template>

このような使い方をするとrequireが暴発して全ての画像を1度index.jsに紐付ける挙動を起こしてしまっていた。そのファイルをWeb上で読み込んでいたので6点になるのも納得である。

上記の問題はrequireをページ側で使うようにして解決し、点数も40点台まで向上した。

<template lang='pug'>
  ...
  CommonImages(:src='require("I/know/what/that/Saitamese/needs.png")' :alt='Feed it some weeds!')
</template>

とんでもない罠である。

その他にもビルドする際に行う画像圧縮の設定ミスを解消したり、Adobeフォントの読み込みを公式が推奨する方法に直したり・・・といったことを行ったがどれもあまり効果は得られなかった。

総論

話を戻すが、今回高得点を目指すのに手元の環境で試した中で最も効果があったのが実はgzipだった。
高得点化において圧縮は偉大なり。圧縮を崇めよ。
また、調査の半分は無駄に終わってしまったが点数の改善という当初の目標を達成できたのは大きい。
結果的には学びも含めて意義ある作業だった。