🐶

Next.js App routerでパフォーマンス・UX改善をする4つの方法

2024/11/24に公開

はじめに

Next.js App routerは、なんとなく開発しているとUXの悪いアプリケーションができてしまいがちです。

ですが、今回紹介する方法を実装すれば、ユーザーがストレスの感じないアプリケーションをそれなりに開発できますので、ぜひみていただければと思います。

対象: Next.js v14

この記事を見て欲しい人

  • Next.js App router 初心者の人
  • Next.js App routerで開発したアプリケーションが、もったりしていて悩んでいる人
  • Next.js App routerで、UXやパフォーマンスの良いアプリケーションを作りたい人

prefetchでページ遷移の速度を上げる

Next.jsのLinkコンポーネントとuse Routerには、prefetchという機能があります。

prefetchを使うと、ページにアクセスする前に、次のページを読み込んでくれるようになります。
ユーザー体験で言うと、クリックした瞬間に次のページが表示されて、SPAのアプリケーションのような早いページ遷移が実現できます。

サンプルコード

// Linkの場合
<Link prefetch href="/contact">
  Contact
</Link>

// routerの場合
const router = useRouter()
router.prefetch('/contact')

参考

https://nextjs.org/docs/app/api-reference/components/link#prefetch

SuspenseでLoading UIを表示させる

app routerでは、非同期コンポーネントでデータフェッチをするスタイルがスタンダードです。

ですがこのままでは、非同期コンポーネントが読み込み中に、ページに何も表示されない状態ですので、ユーザー的には結構しんどいです。

このような時は、Suspenseを使って、読み込み中のUIを表示させましょう。
Suspenseは、非同期コンポーネントが完了するまでの間、別のUIを表示させることができるものです。
Suspenseでコンポーネントをラッピングして使います。

サンプルコード

export default async function ProfilePage() {
  return (
  // ProfileContainerが完了するまで、LoadingUIを表示させる。
    <Suspense fallback={<LoadingUI />}>
      <ProfileContainer />
    </Suspense>
  );
}

async function ProfileContainer() {
  const user = await getCurrentUser();

  if (!user) redirect('/login');

  const userData = {
    id: user.id,
    firstName: user.first_name,
    lastName: user.last_name,
    email: user.email,
    role: user.role,
  };

  return (
    <ProfilePresentation
      user={userForPresentation}
    />
  );
}

参考

https://ja.react.dev/reference/react/Suspense

非同期コンポーネントを分割して、並行データフェッチをする

前述したように、サーバー側で非同期コンポーネントを実装してデータフェッチするのがApp routerのスタンダードな方法なのですが、1つの非同期コンポーネントに複数のデータフェッチを実装すると、直列で1つずつ非同期処理が動きます。

そうなると、読み込み速度に影響が出ます。
そこで、1つの非同期コンポーネントにまとめずに、処理を分割できる場合は、非同期コンポーネントを分割しましょう。

例えば、ブログ記事一覧画面があったとします。
この画面では、記事の一覧とカテゴリの一覧が表示されていると仮定します。
この場合に、記事一覧データをフェッチするコンポーネントと、カテゴリ一覧データをフェッチするコンポーネントを分けると、Next.jsは、並行処理をしてくれます。

Promise.all的なことをしてくれるイメージですね。
前の処理を待たずに、記事一覧とカテゴリ一覧が並行して処理されるので、読み込み速度が速くなります。

サンプルコード

export default function BlogListPage({
  searchParams,
}: {
  searchParams: { page?: string };
}) {
  return (
    <>
      <h1 className="mb-2 px-4 text-xl font-bold">ブログー一覧</h1>
      <BlogListContainer searchParams={searchParams} />
    </>
  );
}

function BlogListContainer({
  searchParams,
}: {
  searchParams: { page?: string };
}) {
  const page = Number(searchParams.page);
  const currentPage = isNaN(page) ? 1 : page;

  return (
    <div>
      <Suspense fallback={<LoadingUI />}>
        <BlogList page={page} />
      </Suspense>
      <Suspense fallback={<LoadingUI />}>
        <CategoryList />
      </Suspense>
    </div>
  );
}

async function BlogList({page}: {
	page: number
}) {
  const blogList = await getBlogList({page});

  return (
    <BlogListUI blogList={blogList} />
  )
}

async function CategoryList() {
  const categoryList = await getCategoryList();

  return (
    <CategoryListUI categoryList={categoryList} />
  )
}

revalidateを利用して、最新のデータをスムーズに取得する

サーバーアクションなどで、データの更新をした後、表示しているデータを最新化する必要があります。

この時には、revalidateを使ってデータの再検証を行うことで、データ更新後にスムーズに新しいデータを表示できます。

revalidatePathとrevalidateTagの2つがありますが、revalidatePathの方がシンプルなので、こちらから使ってみるのがおすすめです。

revalidatePathは、引数で指定したパスを再検証する関数です。

サンプルコード

'use server'

import { createClient } from '@supabase/supabase-js'
import { revalidatePath } from 'next/cache'
import { Database } from '@/types/database.types'

// Supabaseクライアントの初期化
const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function updateMember(formData: FormData) {
  try {
    // フォームデータの取得と検証
    const id = formData.get('id')
    const firstName = formData.get('firstName')
    const lastName = formData.get('lastName')
    const email = formData.get('email')

    if (!id || !firstName || !lastName || !email) {
      return {
      success: false,
      message: '必須項目が入力されていません'
      }
    }

    // メンバー情報の更新
    const { error } = await supabase
      .from('members')
      .update({
        first_name: firstName.toString(),
        last_name: lastName.toString(),
        email: email.toString(),
        updated_at: new Date().toISOString()
      })
      .eq('id', id.toString())

    if (error) {
      throw new Error('メンバー情報の更新に失敗しました')
    }

    // キャッシュの再検証
    // 再検証して、新しいプロフィールデータが取得できる
    revalidatePath('/profile')
    
    return {
      success: true,
      message: '更新成功'
    }
  } catch (error) {
    return {
      success: false,
      message: '更新失敗'
    }
  }
}

参考

https://nextjs.org/docs/app/api-reference/functions/revalidatePath

終わりに

以上です。

Next.jsは、色々な機能がある分、なんとなく使っていると微妙な質のアプリケーションを爆誕させてしまうので、しっかりキャッチアップして、理解して実装できるとGoodですね。

私も今年本格的に使うようになった人間なので、他にも良いtipsがあればご共有いただけますと助かります。

Discussion