本文へスキップ
Suspense

Part 8 · パフォーマンス

46 / 64

Suspense

縮められない待ち時間を、どう見せるか

ここまでのパフォーマンスの話は、むだな描き直しを減らすことでした。

この章は違います。どうしても時間がかかるものを、どう見せるかの話です。

データの取得に 2 秒かかるとき、その 2 秒を短くはできません。 できるのは、その間に何を見せるかを決めることです。

まず動くものを見る

このページを開き直すと、下のカードだけが遅れて出てきます。ページ全体は待っていません。

2 秒かかる一覧

ページを再読み込みすると、ここだけ遅れて出る

周りの文章はすぐ読めています。遅いのはこの箱の中だけで、そこが仕切られています。

この仕切りを作っているのが、たった 3 行です。

<Suspense fallback={<ListSkeleton />}>
  <SlowList />
</Suspense>

slow-list.tsx

/*
  "use client" が付いていない = Server Component。
  サーバー側で await できるので、データが揃うまでの間だけ
  Suspense の fallback が代わりに表示される。
*/

async function getMembers() {
  // 実際の API 呼び出しの代わり。誰の環境でも必ず 2 秒かかる
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return ["田中", "佐藤", "鈴木", "高橋"];
}

export async function SlowList() {
  const members = await getMembers();

  return (
    <ul className="flex h-24 flex-wrap items-center gap-2">
      {members.map((member) => (
        <li key={member} className="rounded-md border px-3 py-1.5">
          {member}
        </li>
      ))}
    </ul>
  );
}

サーバーで待つ

デモの中身は、こう書かれています。

export async function SlowList() {
  const members = await getMembers();
  return <ul>...</ul>;
}

コンポーネントが async になっていて、 中で await しています。もしこれを、ブラウザ側で自分で書くとどうなるか。 (この書き方は Part 9 で詳しく扱います)

// useEffect で取る場合、state が 3 つ要る
const [members, setMembers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

// async な Server Component なら、この 1 行
const members = await getMembers();

読み込み中の state がありません。「読み込み中かどうか」を Suspense が外側で受け持つからです。 中のコードは、データが揃った後のことだけ書けばよくなります。

slow-list.tsx

/*
  "use client" が付いていない = Server Component。
  サーバー側で await できるので、データが揃うまでの間だけ
  Suspense の fallback が代わりに表示される。
*/

async function getMembers() {
  // 実際の API 呼び出しの代わり。誰の環境でも必ず 2 秒かかる
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return ["田中", "佐藤", "鈴木", "高橋"];
}

export async function SlowList() {
  const members = await getMembers();

  return (
    <ul className="flex h-24 flex-wrap items-center gap-2">
      {members.map((member) => (
        <li key={member} className="rounded-md border px-3 py-1.5">
          {member}
        </li>
      ))}
    </ul>
  );
}

fallback は「同じ形の型」にする

fallback には好きなものを置けます。 ですが置き方に良し悪しがあります

このデモの fallback は、本体と同じ高さ(h-24にしてあります。 だから 2 秒後に差し替わっても、下の文章が動きません。

animate-pulse でうっすら点滅させているのも意味があります。止まっているのではなく、待っていると伝えるためです。

skeleton.tsx

/*
  待っている間に置いておく「型」。
  本体と同じ高さ (h-24) にしてあるので、
  差し替わった瞬間に周りがガタッと動かない。
*/

export function ListSkeleton() {
  return (
    <ul className="flex h-24 flex-wrap items-center gap-2" aria-hidden>
      {[3, 4, 3, 4].map((width, index) => (
        <li
          // 見た目だけの飾りなので index を key にしてよい(並び替わらない)
          key={index}
          className="h-8 animate-pulse rounded-md bg-muted"
          style={{ width: `${width}rem` }}
        />
      ))}
    </ul>
  );
}

どこに置くか

Suspense仕切りです。 置いた場所より内側だけが待ちます。

// ✕ ページ全体を包む → 何も見えないまま 2 秒
<Suspense fallback={<Spinner />}>
  <Header />
  <Article />
  <SlowList />
</Suspense>

// ◯ 遅いところだけ包む → 他は即座に出る
<Header />
<Article />
<Suspense fallback={<ListSkeleton />}>
  <SlowList />
</Suspense>

遅いものに、できるだけ近いところで包みます。包む範囲が広いほど、待たされる範囲も広がります。

逆に、遅いものが 2 つあって片方だけ先に出したいなら、 それぞれ別々に包みます。早く終わったほうから順に出てきます。

slow-list.tsx

/*
  "use client" が付いていない = Server Component。
  サーバー側で await できるので、データが揃うまでの間だけ
  Suspense の fallback が代わりに表示される。
*/

async function getMembers() {
  // 実際の API 呼び出しの代わり。誰の環境でも必ず 2 秒かかる
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return ["田中", "佐藤", "鈴木", "高橋"];
}

export async function SlowList() {
  const members = await getMembers();

  return (
    <ul className="flex h-24 flex-wrap items-center gap-2">
      {members.map((member) => (
        <li key={member} className="rounded-md border px-3 py-1.5">
          {member}
        </li>
      ))}
    </ul>
  );
}

理解できたか確かめる

確認クイズ

Suspense を使うと何が変わる?

確認クイズ

async な Server Component で読み込み中の state が要らないのはなぜ?

確認クイズ

fallback として適切なのは?

slow-list.tsx

/*
  "use client" が付いていない = Server Component。
  サーバー側で await できるので、データが揃うまでの間だけ
  Suspense の fallback が代わりに表示される。
*/

async function getMembers() {
  // 実際の API 呼び出しの代わり。誰の環境でも必ず 2 秒かかる
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return ["田中", "佐藤", "鈴木", "高橋"];
}

export async function SlowList() {
  const members = await getMembers();

  return (
    <ul className="flex h-24 flex-wrap items-center gap-2">
      {members.map((member) => (
        <li key={member} className="rounded-md border px-3 py-1.5">
          {member}
        </li>
      ))}
    </ul>
  );
}

この章のまとめ

  • Suspense待つ範囲を仕切る境界線。 速くする道具ではない
  • async な Server Component なら、読み込み中の state が要らなくなる
  • fallback は本体と同じ寸法の型にする。 でないと差し替わった瞬間に画面が跳ねる
  • 遅いものに、できるだけ近いところで包む

slow-list.tsx

/*
  "use client" が付いていない = Server Component。
  サーバー側で await できるので、データが揃うまでの間だけ
  Suspense の fallback が代わりに表示される。
*/

async function getMembers() {
  // 実際の API 呼び出しの代わり。誰の環境でも必ず 2 秒かかる
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return ["田中", "佐藤", "鈴木", "高橋"];
}

export async function SlowList() {
  const members = await getMembers();

  return (
    <ul className="flex h-24 flex-wrap items-center gap-2">
      {members.map((member) => (
        <li key={member} className="rounded-md border px-3 py-1.5">
          {member}
        </li>
      ))}
    </ul>
  );
}