Part 8 · パフォーマンス
46 / 64 章
Suspense
縮められない待ち時間を、どう見せるか
ここまでのパフォーマンスの話は、むだな描き直しを減らすことでした。
この章は違います。どうしても時間がかかるものを、どう見せるかの話です。
データの取得に 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>
);
}