t0mmy

2020年12月30日に参加

学習履歴詳細

実践Next.js 10章 パフォーマンスとキャッシュ 読了

やったこと

  • 実践Next.js 10章 パフォーマンスとキャッシュ 読了

学んだこと

並列化でパフォーマンス改善

依存関係にないデータの取得は、 並行でデータを取得することで、パフォーマンスを改善できる。

◆直列取得

const data1 = await getData1();
const data2 = await getData2();
const data3 = await getData3();

◆Promise.all による並列取得

const [data1,data2,data3] = await Promise.all([
  getData1(),
  getData2(),
  getData3()
])

◆子サーバーコンポーネントに分割して並列取得

import { data1_component } from "..."
import { data2_component } from "..."
import { data3_component } from "..."

...
  return (
  <>
    <data1_component />
    <data2_component />
    <data3_component />
  </>
  )

データ取得処理用に 子 Server Component を用意し、データ取得処理を委譲する。
コンポーネント構造でも、データを並列取得するため、パフォーマンス改善が見込める。

  • 可読性向上や再利用性向上といったメリットもある

Server Component の動作に必要なデータが依存関係にない場合、子 Server Component に分割すると良い。

fetchCache の設定

fetch 関数のキャッシュデフォルト設定は auto
これは、「動的関数が使用された」ことを起因に、キャッシュの挙動を切り替えるというもの。
fetchするデータと無関係な動的関数が使用された場合でも、動的データ取得扱いされ、リクエストが発生する恐れがある。

  • 無関係な動的関数の例 : ログインしているユーザーの情報を取得するための getServerSession 関数

この場合、 fetch 関数の キャッシュ設定を force-cache とすると、常に静的データ取得扱いされ、常にキャッシュされるようになる。

静的 Route を増やす実装

無関係な動的関数の使用、静的Route であってほしい Route が、意図せず動的 Route になってしまうことがある。

  • ログインしているユーザーの情報を取得するための getServerSession 関数など
  • Page のほかに、Layout.tsx で動的関数を使用している場合でも、動的 Route 扱いされる

動的 Route の原因となっている、 動的関数を取り除くために、以下の方法が採用できる。

  • 動的関数を使用する部分を Client Component として 別コンポーネントに切り出す

動的関数を含む処理を CSR にて処理するよう修正することで、Route が 静的 Route を増やし、パフォーマンス改善につなげることが出来る。

ただし、動的データ取得処理が多い場合、 Server Component でまとめて取得する 動的 Route の方が、結果的に高パフォーマンスに繋がることもある。

動的関数 in CSR + 静的Route が、常に最適解というわけではない。

SSG Route の実装

予め Route のレンダリング結果を出力する SSG (またはISR)は、パフォーマンス改善において効果の高い手法。

SSG Route を提供する

generateStaticParams 関数を使用する。

  • SSGしたいパスのパスパラメータを定義する関数
  • generateStaticParams 関数を export することで、 Next.js に認識される
  • ビルド時にしか実行されない

例)

export function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }, { id: '3' }]
}

// Three versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
// - /product/1
// - /product/2
// - /product/3
export default function Page({ params }: { params: { id: string } }) {
  const { id } = params
  // ...
}

/product/1/product/2/product/3 は、SSGにより、ビルド時に事前レンダリングされる。
他のパラメータ( /product/4 , /product/5 など)は、リクエストが来たらレンダリングされ、レスポンスを返すと同時にSSGが行われる。

generateStaticParams 関数は、非同期として定義することもできる。
非同期関数として定義し、DBから SSGしたいID情報を取得し、return に設定する ... といった使い方が想定される。

generateStaticParams に複数の値を持たせたい場合、パラメータ部分を配列にするだけ。

export function generateStaticParams() { 
  return [{ slug: ['a', '1'] }, { slug: ['b', '2'] }, { slug: ['c', '3'] }]
}

パスパラメータとなる変数が複数存在する場合、その数だけ key-value を定義する

export function generateStaticParams() {
  return [
    { category: 'a', product: '1' },
    { category: 'b', product: '2' },
    { category: 'c', product: '3' },
  ]
}
// Three versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
// - /products/a/1
// - /products/b/2
// - /products/c/3
export default function Page({
  params,
}: {
  params: { category: string; product: string }
}) {
  const { category, product } = params
  // ...
}

SSG を考慮したURI設計

パスにクエリパラメータを含め、 searchParams()を使用する ... というコードだと、 searchParams() が動的関数のため、動的Route と判断される。

  • categories/flower?page=1 など

これを避けてSSG Route とした扱いたい場合、クエリパラメータではなくパスパラメータへ変更する...といった改善策がある。

  • categories/flower/1 など

参考 : https://nextjs.org/docs/app/api-reference/functions/generate-static-params#single-dynamic-segment

Route Segment Config

Route Segment 単位で、レンダリングやキャッシュ設定を一括制御できる機能。
特定の Route Segment に設定すると、 Subtree 全てに、設定内容を反映できる。
以下のように、変数をexport するだけで一括指定出来る。

export const revalidate = false // false | 'force-cache' | 0 | number

SSG Route パフォーマンスの定量評価

以下の評価項目がある

いずれも、開発者ツールで確認できる。

Next.js によるアセットの最適化

画像の最適化

<Image> コンポーネントの使用

Next.js が提供する <Image> コンポーネントを使用する。
これは html の <image> 要素を拡張したコンポーネントであり、以下の最適化が自動で適用される。

  • 容量の最適化 : 配信に最適な画像形式(webpやavifなど)へ変換
  • 視覚上の安定化 : 画像読み込み時、レイアウトずれの発生を自動防止
  • 遅延読み込み : 初期表示ではプレースホルダー を表示し、ビューポートに入った時に読み込みを行う
    • プレースホルダー表示ため、 alt 属性が必須となっている(空文字列でもOK。基本は画像を説明する文字列)
  • 画像サイズ変更 : 画像サイズをオンデマンドで変更する

更に最適化された画像ファイルは、Next.js によってキャッシュされる。

  • キャッシュの格納先は .next/cache/images フォルダ

画像サイズ指定

画像サイズを指定することにより、 「Layout Shift」 というパフォーマンス問題を回避できる。

◆Layout Shift
画像読み込みによって、他のHTML要素を動かしてしまう事象。
画像表示に必要なスペースを、事前に設定しておくことで回避できる。

画像サイズの設定必須。

優先読み込みを指定できる priority props

priority を追加すると、追加された<Image>の画像は、優先的に読み込むことが出来る。
これにより、LCP向上につなげることが出来る。

画像の読み込み先を制限する

「信頼できる画像読み込み先」を指定し、信頼できない画像読み込み先との通信をブロックする。

これは、next.configにて設定する。

{
const nextConfig = {
    images: 
      { remotePatterns: 
        [ 
          { 
            protocol: "https", 
            hostname: "lh3.googleusercontent.com", 
          },
          { 
            protocol: "http", 
            hostname: "127.0.0.1", 
          },
        ],
     },
}; 
export default nextConfig;
}

画像フォーマット指定

next.config にて、画像形式を指定できる。

const nextConfig = {
  images: {
    formats:['image/avif','image/webp']
  }
}

未指定の場合、 image/webp が採用される。

画像のキャッシュ時間を指定する

最適化された画像は Next.js によってキャッシュされる。
このキャッシュ時間を、 next.config にて指定できる。

const nextConfig = {
  images: {
    minimumCacheTTL: 60
  }
}

画像キャッシュを無効化することはできない。
キャッシュ保持期間を可能な限り短くすることで、暫定対応する。

キャッシュ時間は、上記 minimumCacheTTL の値と、サーバーレスポンスの max-age Cache Control ヘッダの内、大きい値が採用される。

フォントの最適化

next/font を使用すると、あらゆる Font ファイルのセルフホスティングが可能。
従来のようにFontファイル とCSSファイルの準備が不要になる。

サブセットの指定

Google Font は、自動的にサブセット化できる。

◆サブセット化
使用している文字だけ抜き出しフォント化すること

サブセット化により、Fontファイルのサイズを削減し、パフォーマンス向上が見込める。

const inter = Inter ({subsets: ["latin"])

CSS Variables の適用

外部CSS を読み込み、一部オプションを追加して使いたい場合は、CSS Variables を使用する。

import { Inter } from "next/font/google"; 
import styles from "../styles/component.module.css"; 

const inter = Inter({ variable: "--font-inter", });

スタイルを設定するテキストの「親コンテナのclassName」 をフォントローダーの変数値に設定する。
そして、テキストのclassNameを外部CSSファイルのセレクターが適用されるように設定する。

<main className={inter.variable}> 
  <p className={styles.text}>Hello World</p> 
</main>

そして、外部CSSファイル(component.module.css)で、以下のようにCSS Variables を使用する。

.text { font-family: var(--font-inter); 
font-weight: 200; 
font-style: italic; }

フォント定義ファイルの使用

フォントファイルは、ロードする度にインスタンス生成処理が走る。
何度もインスタンス生成処理が走るのは非効率。

そのため、フォントファイルのロード処理は一か所にまとめておき、 exportする。
他のコンポーネントは、importするだけにしておく。

サードパーティスクリプト読み込みの最適化

Layout Script

複数のRoute でサードパーティスクリプトを読み込む場合は、
next/script をインポートし、 <Scriptl> コンポーネントを Layoutコンポーネントに含めるよう定義する。

これにより、 Layout側で一度スクリプトをダウンロードするだけで、 Subtree でもスクリプトを使用できる。

Root Layout に含めることで、全ての Route で使用するスクリプトを定義する事もできる。

不必要なスクリプトのダウンロードは、パフォーマンスの悪化につながる。
基本的に、サードパーティスクリプトのダウンロードは、スクリプトが必要な特定のページまたはサブコンポーネントで行う。
全ページで必要なサードパーティスクリプトのみ、 Root Layout でダウンロードする。

Next.js の 4種類のキャッシュ

Next.js は、レンダリング結果とデータ取得結果をキャッシュする。
以下の順番でキャッシュする。

  1. Request のメモ化
  2. Data キャッシュ
  3. Full Route キャッシュ
  4. Router キャッシュ

Request のメモ化

fetch 関数の戻り値(取得データ)を一時的にキャッシュする。
サーバー上で重複して呼び出された fetch 関数は、キャッシュした戻り値を再利用する。

同一のfetch関数リクエスト排除を目的としており、これを「メモ化」と呼ぶ。
これは、fetch 関数を使用して同一のリクエストを試みても、リクエストは1回のみに抑えることを意味する。
これにより、コンポーネント単位でデータ取得関数を呼びだしても、キャッシュが機能するようになる。

このメモ化は、レンダリング中のみ有効。

  • 言い換えると、レンダリングが完了すると、メモ化したデータはリセットされる

fetch 関数以外で Request をメモ化したい場合は、 React の cache 関数を使用する。

Data キャッシュ

ビルド時、fetch 関数の取得データを永続的にキャッシュする。

当然、変更のない "静的データ" がキャッシュの対象となる。

キャッシュの有効期限は、 Time-based Revalidation の設定で設けることが出来る。
また、On-demand Revalidation にて、能動的に無効化する事もできる。
force-cache されたデータは、再ビルド時でもキャッシュは削除されず、ずっと再利用され続ける。

ローカル開発だと、Dataキャッシュを直接削除する( .next/cache/fetch-cache 配下)ことで、キャッシュを削除できる。

以下のように設定することで、プロジェクトを通して「キャッシュを使用しない」ことを設定できる。

// Segment Subtree 配下のデータ取得を、全て動的へ
export const dynamic = "force-dynamic";
// Segment Subtree 配下のデータ取得時、一切キャッシュしない
export const revalidate = 0;

fetch 関数以外で Data キャッシュを作成したい場合、Next.js の unstable_cachce 関数を使用する。
第一引数に渡した非同期関数の戻り値が、キャッシュされるようになる。

Full Route キャッシュ

各 Route のレスポンスを永続的にキャッシュする。
Full Route キャッシュが存在せず、Routeでデータ取得がある場合、「Data キャッシュ」を経由してDataソースにアクセスする。

静的サイト生成(SSG)や静的レンダリングファイルのこと。

キャッシュはビルド時、またはリクエスト時に作成される。
再ビルドするまで、ずっと残る。

  • 言い換えると、Full Route キャッシュを削除するには、再ビルドが必要

ただし、 Dataキャッシュの Revalidate が発生すると、関連する Full Route キャッシュも無効になる。
無効になった Full Route キャッシュは、 Dataキャッシュが作成されるときに、同時に作成される。

Router キャッシュ

ブラウザにて保持するキャッシュ。

主な目的は、「戻る」「進む」ボタン処理を高速にしつつ、データアクセスの頻度を削減すること。

一定期間が過ぎると、部分的なRouter キャッシュは自動で無効になる。
キャッシュの有効期間は以下の通り。

  • 動的レンダリングRoute : 30s
    • <Link> コンポーネントの props に prefetch={true} を使用すると、有効期間が5分に延長される
  • 静的レンダリングRoute : 300s (5分)
    • <Link> コンポーネントの props に prefetch={false} を使用すると、有効期間が30秒に短縮される

この有効期間は切り替え可能。

能動的にキャッシュを無効化するには、以下のいずれかの手段を取る。

  • ブラウザで router.refresh を実行する
    • http リクエストが二往復必要になる
  • Server Action 内で coolies.set または cookies.delete を使用する
    • 古い Full Route キャッシュやDataキャッシュにヒットし、意図しない結果につながる可能性がある
  • Server Action 内で on-demand revalidation を実施する
    • 推奨
Next.js

2024年06月02日(日)

3.0時間