23
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

直近でパフォーマンスチューニングのタスクを実施させていただいていますが、現状までに行った対応策を記事にまとめようと思います。

本記事では、フロントエンドにおけるパフォーマンスチューニングの方法について記載します。
また、Nuxt と Vite におけるチューニング方法をまとめます。

実施内容

  • Component のダイナミックインポート(非同期インポート)
  • 画像の非同期読み込み
  • 画像の圧縮
  • デバイスごとに画像サイズを最適化
  • chunk の細分化
  • フレームワークのバージョンアップ対応
  • サードパーティスクリプトのドメイン事前接続
  • 重い API における、データ表示のスケルトン実装

Component のダイナミックインポート(非同期インポート)

こちらは、Nuxt であれば LazyXXX でコンポーネントを非同期読み込みする方法や、Vue であれば defineAsyncComponent で非同期読み込みする方法になります。

<template>
  <LazyXXX />
</template>

<script setup lang="ts">
import LazyXXX from '@/components/XXX/index.vue'
</script>

LazyXXX の定義をする際に、nuxt.config.tscomponents: false だと使用できないので、その点にご注意ください。

<template>
  <LazyXXX />
</template>

<script setup lang="ts">
const LazyXXX = defineAsyncComponent(() => import('@/components/XXX/index.vue'))
</script>

主に、ビューポートから溢れたコンポーネントや、サードパーティ製のパッケージにおける UI コンポーネント(例えば chart.js など)に対して非同期読み込みを実施しました。

こちらの対応によって、build 後の js のサイズが縮小される効果を得られます。
非同期読み込みを実装することで、メインの js から非同期コンポーネントの処理が別の chunk に切り出されるためです。
こちらの対応によって、FCP / LCP の改善が見込めます。

画像の非同期読み込み

こちらはよくあるパフォーマンスチューニング内容かと思います。
単純に img 要素に対して、loading="lazy" を付与するだけの対応になります。
ただ、その対応だけでもパフォーマンスが大きく上がったりします。
下記は NuxtImg を使用したサンプルコードになります。

<NuxtImg alt="sample" src="/public/sample.png" loading="lazy" width="100" height="100" />

SEO の観点で altwidth / height の指定も必須で行うようにしましょう。
sizes などの指定も必要ですが、そちらはこの後に記載します。
こちらの対応によって、FCP / LCP の改善が見込めます。

画像の圧縮

画像は注意しないとファイルサイズの大きなものをコミットしてしまったりします。
そうならないように webp など、よりファイルサイズが小さな画像となるように圧縮することも大事です。
NuxtImg を使用していれば format 属性で変換できたりしますが、パッケージによる圧縮で画像が崩れる可能性もあるため、cloudconvert などを使用して事前に webp に変換します。
こちらの対応によって、FCP / LCP / TTFB の改善が見込めます。

デバイスごとに画像サイズを最適化

こちらは img タグにおける sizes / srcset の指定となります。
レスポンシブ対応のアプリケーションでは画像はそれぞれのブレイクポイントで適切なサイズを指定することが重要となります。
こちらに関しても NuxtImg を使用すれば簡単に設定できます。
必要に応じて、picture (Nuxt であれば NuxtPicture)タグの使用も検討します。
こちらの対応によって、FCP / LCP の改善が見込めます。

chunk の細分化

こちらは Vite の設定になります。
Nuxt3 から split chunk が実装され、細かく chunk が分けられるようにビルドされるようになりましたが、アプリケーションが大規模になるにつれて、エントリーポイントの js が非常に大きくなったりします。
その際に、Vite の設定で chunk をさらに細かく分割する設定を追加します。
下記は nuxt.config.ts における vite の chunk 設定例です。(あくまでもサンプルコードです)

nuxt.config.ts
const libraryChunkMapping: Record<string, string[]> = {
  'vue-router-libs': ['vue-router'],
  'date-libs': ['dayjs'],
  'data-libs': ['lodash-es']
}

export default defineNuxtConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks(id) {
            const libraryChunkName = Object.keys(libraryChunkMapping).find((key) => {
              return libraryChunkMapping[key].some((lib) => id.includes(lib))
            })
            if (libraryChunkName !== undefined) {
              return libraryChunkName
            }
          }
        }
      }
    }
  },
  // ... 省略
})

vite では 500kb を超えると、ビルド時に警告が出ますが、上記の対応を細かく設定することでその警告を出さないように修正することもできてます。
上記の対応により、FCP / LCP / TTFB などの改善が見込めます。

1点だけ注意が必要ですが、こちら、chunk ファイルの定義を記載するのはいいですが、非同期コンポーネントで使用しているソースは含めないように設定を書く必要があります。
というのも上記で設定した chunk は画面のファーストロードで必ず読み込まれるため、せっかく非同期コンポーネントにして非同期で読み込むように設定しても、js をブロックしてしまいます。

フレームワークのバージョンアップ対応

使用しているフレームワークも日々パフォーマンス改善をしてくれています。
そのため、アップグレードできるなら積極的にしておくと良いでしょう。
フレームワークはもちろん、アプリケーションで使用しているユーティリティや UI パッケージ等もリリースノートを確認し、アップグレードすることもパフォーマンス改善につながります。
実際に Nuxt のバージョンアップをしたら、パフォーマンスのスコアが微量ながらも向上しました。

サードパーティスクリプトのドメイン事前接続

こちらは、頻繁にアクセスする外部ドメインへの接続を事前に行う対応となります。
そうすることで、DNS ルックアップの事前実行や TCP ハンドシェイクの事前確立を行うことができます。
下記は nuxt.config.ts で Google Tag Manager に対する事前接続の設定方法となります。

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

その他にも、外部フォントの配信元ドメインや、マイクロサービスアーキテクチャであれば、API 側のドメインも追加しておくといいでしょう。
ただ、設定しすぎも良くありません。
Chrome / Chromium 系では最大6個までなどの制約があったりするので、サポートブラウザに応じて適切な数に絞り、優先度の高いものだけを設定するようにしましょう。

使用フォントの通信をなくす

本アプリケーションでは Google font を使用していました。
そのフォントの読み込み方法を下記のようにしていました。

nuxt.config.ts
export default defineNuxtConfig({
  // ... 省略
  app: {
    head: {
      link: [
        {
          rel: 'preconnect',
          href: 'https://fonts.googleapis.com'
        },
        {
          rel: 'preconnect',
          href: 'https://fonts.gstatic.com',
          crossorigin: ''
        },
        {
          rel: 'stylesheet',
          href: 'https://fonts.googleapis.com/css2?family=Roboto+Condensed&family=Noto+Sans:wght@100..900&display=swap'
        }
      ]
    }
  }
  // ... 省略

そのため、HTTP 通信するやり方ではなく、フォントの CSS 自体をローカルに落とすよう、nuxt module の @nuxtjs/google-fonts を使用し、下記の設定に変更しました。

nuxt.config.ts
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
  },
  // ... 省略

上記の変更により、FCP / LCP が約1秒ほど改善することができました。

重い API における、データ表示のスケルトン実装

こちらは、他にチューニングの余地がない場合や、施策の実施に半年以上の工数がかかるといった状況で検討する施策です。
UI の変更を伴うため、プロダクトマネージャー(PdM)やデザイナーに事前に確認をとってから実装するようにします。
マイクロサービスなどでは、API のレスポンス速度がどうしても遅くなってしまうものがあったりします。
その場合、該当データを表示している箇所を非同期で取得するように変更し、取得中はスケルトンUIを表示するようにします。
下記はスケルトン UI のイメージです。

1_D1D_LeQm6o_6prrIi2Jw3w.gif

まとめ

パフォーマンスチューニングは現在も継続して実施していますが、本記事では途中までの内容をまとめました。
ここまでの対応で、PageSpeed Insights でのパフォーマンススコアが 10〜30%ほど向上しています。
本記事が、他の方のパフォーマンスチューニングのヒントになれば幸いです。

23
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?