効果別に解説!Nuxt.jsで実現するWebサイトパフォーマンス改善の施策と実装例

この記事で解決できるお悩み
  • パフォーマンス改善の施策にはどういったものがある?
  • より効果的な施策には何がある?
  • パフォーマンス改善でやるべきことは分かったが、Nuxt.jsでの実装方法が分からない

本記事では、フロントエンドのパフォーマンス改善に焦点を当て、特にNuxtを使用している方向けに、具体的な施策とその実装例をご紹介します。

目次

サイトパフォーマンス改善の施策一覧と効果

スクロールできます
施策名主な効果指標効果度施策内容期待される効果
画像の最適化FCP
LCP
CLS
widhth/heightの指定、画像のサイズ圧縮、WebP形式への変換、Lazy Load、読み込み優先度の指定画像のダウンロード時間短縮、LCP(ページの表示速度)の改善、レイアウトシフトの防止
CSSの最適化FCP
LCP
クリティカルCSSのインライン化、非クリティカルCSSの遅延読み込み、不要なCSSの削除、CSSファイルの分割FCP(最初の描画速度)の改善、レンダリングブロックの解消
Google Fontsの最適化FCP
LCP
TBT
フォントファイルのローカルホストFCP(最初の描画速度)の改善、レンダリングブロックの解消
リソースの事前圧縮FCP
TTFB
CSS/JSをビルド時に圧縮するファイルダウンロード時間の短縮、FCP(最初の描画速度)の改善
サードパーティのドメイン事前接続FCP
TTFB
サードパーティスクリプトのドメインを事前接続するスクリプトのダウンロード開始までの時間を短縮
コンテンツ遅延読み込みFCP初期描画時に不要なコンポーネント(コンテンツ)の遅延読み込みFCP(最初の描画速度)の改善

以下は実現が難しく、途中で断念した施策です。

スクロールできます
施策名主な効果指標効果度施策内容期待される効果
サードパーティのメインスレッドブロック削減FCP
LCP
TBT
Google Analyticsなどのサードパーティスクリプトをworkerスレッドで実行するメインスレッドブロックの解消
リソースの事前読み込みFCP初期描画時に使用するリソース(画像など)を事前に読み込むFCP(最初の描画速度)の改善

Nuxt.jsでの実装例

画像の最適化

@nuxt/imageモジュールを使用すると、画像の圧縮やサイズ最適化を簡単に実装できます。

STEP
@nuxt/imageをインストール
npm i @nuxt/image
STEP
nuxt.config.tsにモジュールを追加して有効化
export default defineNuxtConfig({
  modules: ['@nuxt/image']
})
STEP
NuxtImgを使用して画像を表示

LCP判定されている画像は以下サンプルコードのようにfetchpriorityとpreloadをつけることを推奨します。また、ファーストビューで表示しない画像はloading="lazy"をつけると良いでしょう。

<NuxtImg
  src="/img/hero.png"
  alt="ヒーロー画像"
  sizes="sm:290px md:400px"
  width="400px"
  height="600px"
  :preload="{ fetchPriority: 'high' }"
  fetchpriority="high"
/>

CSSの最適化

CSSファイルの分割

Nuxt.jsではデフォルトでCSSファイルが分割されるようになっています。
CSSファイルが分割されることで1ファイルあたりのサイズが減り、処理時間の短縮が見込めます

非クリティカルCSSの遅延読み込み

Nuxt.jsでは、デフォルトでよしなにCSSのインライン化と外部ファイル化をしてくれています。
ただ、外部ファイルのCSSは初期描画時にレンダリングをブロックするので、FCP悪化の大きな原因になります。

Nuxt.jsではnuxt-modules/crittersというモジュールがありますが、私が試したところデグレを起こしたため、外部ファイルのCSSをレンダリングブロックさせない処理を独自実装しました。

以下のコードをserver/plugins配下に配置してください。

import isString from 'lodash-es/isString'

/** CSS link regex */
const CSS_LINK_REGEX = /<link([^>]*?)rel="stylesheet"([^>]*?)href="([^"]+\.css)"([^>]*?)>/g
/** media属性を取得するための正規表現 */
const MEDIA_ATTRIBUTE_REGEX = /media="([^"]+)"/
/** media属性を削除するための正規表現 */
const MEDIA_ATTRIBUTE_REMOVE_REGEX = /\s*media="[^"]+"/g

/**
 * CSS link要素の変換処理
 * @param {string} _ - マッチした文字列(使用しない)
 * @param {string} beforeRel - rel属性の前の部分
 * @param {string} betweenRelHref - rel属性とhref属性の間の部分
 * @param {string} href - href属性の値
 * @param {string} afterHref - href属性の後の部分
 * @returns {string} 変換後のlink要素文字列
 */
const transformCssLink = (
  _: string,
  beforeRel: string,
  betweenRelHref: string,
  href: string,
  afterHref: string
): string => {
  const fullTag = `<link${beforeRel}rel="stylesheet"${betweenRelHref}href="${href}"${afterHref}>`
  const mediaMatch = MEDIA_ATTRIBUTE_REGEX.exec(fullTag)
  const originalMedia = mediaMatch?.[1] ?? 'all'
  const lazyLink = fullTag
    .replace(MEDIA_ATTRIBUTE_REMOVE_REGEX, '')
    .replace(
      />$/,
      ` media="none" onload="if(media!=='${originalMedia}')media='${originalMedia}'" fetchpriority="high">`
    )

  return `${lazyLink}<noscript>${fullTag}</noscript>`
}

/**
 * 非クリティカルCSSをレンダリングブロックせずに読み込むためのプラグイン
 */
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html) => {
    html.head = html.head.map((headItem) => {
      return isString(headItem) ? headItem.replace(CSS_LINK_REGEX, transformCssLink) : headItem
    })
  })
})

lodash-esをインストールしていない場合は代替えのコードに置き換えるかlodash-esをインストールしてください。

では、先ほどのコードを解説します。

<link rel="stylesheet">タグにmedia="none"を設定してレンダリングブロックを無効化し、CSSファイルが読み込み完了した時点でonloadイベントでmedia="all"に変更して適用します。

またfetchpriority="high"を付与することで、読み込みタイミング自体は変えずにレンダリングブロックのみを回避しています。

Google Fontsの最適化

Google Fontsは便利ですが、外部からの読み込みはFCPを遅延させる原因になります。

Nuxt.jsでは、@nuxtjs/google-fontsを使用してフォントファイルをローカルにホストすることで、初期描画時のレンダリングブロックを回避することができます。

STEP
@nuxtjs/google-fontsをインストール
npm i @nuxtjs/google-fonts
STEP
nuxt.config.tsにモジュールを追加して有効化

googleFontsfamiliesやその他のオプションは適宜変更してください。

export default defineNuxtConfig({
  modules: ['@nuxtjs/google-fonts'],
  googleFonts: {
    families: {
      'Roboto Condensed': true,
      'Noto Sans': {
        wght: '100..900'
      }
    },
    display: 'swap',
    prefetch: false,
    preconnect: false,
    preload: true,
    download: true,
    base64: false,
    useStylesheet: true,
    overwriting: true
  }
})

上記の設定を追加することで、ビルド時にフォントファイルがローカルにダウンロードされます。

リソースの事前圧縮

CSSとJavaScriptのサーバー応答時間を短縮するために、ビルド時にファイルの圧縮を行います。

nuxt.config.tsに設定を追加して事前圧縮を有効化

export default defineNuxtConfig({
  nitro: {
    compressPublicAssets: {
      gzip: true,
      brotli: true
    }
  }
})

サードパーティのドメイン事前接続

「Script Evaluation」 の前の段階であるスクリプトのダウンロード開始までの遅延を減らすため、サードパーティドメインへ事前接続を行います。

nuxt.config.tsに設定を追加します。
以下はGoogle Tag Managerのドメインの設定例です。

export default defineNuxtConfig({
  link: [
    {
      rel: 'preconnect',
      href: 'https://www.googletagmanager.com'
    },
  ]
})

コンテンツ遅延読み込み

初期描画時に必要のないコンポーネントをdefineAsyncComponentで遅延読み込みさせます。

const footerCta = defineAsyncComponent(() => import('@/components/footer/cta.vue'))

サードパーティのメインスレッドブロック削減

この施策は実現が難しく、現状はまだpendingになっていますが、どこまで調査したかも含めて記述しておきます。

やりたいこととしては、「workerスレッドでサードパーティスクリプトを実行してメインスレッドのブロック時間を削減する」です。

partytownというモジュールを使用すれば、実現できそうだったのですが、GTM経由でGAなどを実装する場合は、CORSエラーになるみたいです。(対象のissueはこちら

もしかするとリバースプロキシを導入すれば、解消されるかもしれないのですが、GTM以外のサードパーティも上手く動作しなかったので、モジュールのアップデートを待つという結論になりました。

リソースの事前読み込み

この施策も実現が難しく、現状はまだpendingになっていますが、どこまで調査したかも含めて記述しておきます。

やりたいこととしては、「初期描画時に必要な画像などのリソースをhtmlのレスポンス前に読み込みを開始する」です。

NuxtConfigのオプションのEarly Hintsで実現できそうだったのですが、実験的機能ということもあり、動作が安定しなかったため、機能のアップデートを待つという結論になりました。(対象のissueはこちら)

まとめ

最後まで読んでいただき、ありがとうございました!

限られた工数では、効果の高い施策から優先的に取り組むことが重要です。

ユーザビリティ向上のため、ぜひハイパフォーマンスなサイト開発にお役立てください。

この記事が気に入ったら
フォローしてね!

シェアしていただけると大変励みになります!
  • URLをコピーしました!
  • URLをコピーしました!
目次