Part 8 · パフォーマンス
42 / 64 章
children で切り離す
memo を使わずに、描き直しの範囲を狭める
前の章で memo をやりました。 ですが、memo を使わずに描き直しを止める方法があります。
しかも新しい道具は要りません。使うのは Part 2 でやった children です。
知っている人と知らない人で、書くコードがはっきり変わる話です。
まず、ふつうに書いた場合
state を持つコンポーネントの中に、重い子を置きます。 いちばん自然な書き方です。
count を押すと、子も一緒に光る
count しか変えていないのに、子も毎回描き直されています。 Part 7 でやったとおり、親が描き直されれば子も描き直されるからです。
flat.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { useState } from "react";
// 重い処理をしている想定の子
function Heavy() {
return <RenderBox title="重い子">親の中で作られている</RenderBox>;
}
export function Flat() {
useTrackDemoRender();
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count: {count}
</Button>
{/* この行は Flat の中にある。Flat が描き直されるたびに作り直される */}
<Heavy />
</div>
);
}子を外で作って、渡す
同じものを、子を外側で作ってから渡す形に書き換えます。 子の中身には手を触れません(表示する文言だけ、どちらの形か分かるように変えてあります)。
// 変更前:子を中で作る
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<Button onClick={...}>{count}</Button>
<Heavy />
</div>
);
}
// 変更後:子を受け取って置くだけ
function Counter({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<Button onClick={...}>{count}</Button>
{children}
</div>
);
}count を押しても、子は光らない
子が光らなくなりました。memo は使っていません。 子の中身も変えていません。
split.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
return (
<RenderBox title="重い子" tone="highlight">
外で作られて、children として渡されている
</RenderBox>
);
}
// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count: {count}
</Button>
{/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
{children}
</div>
);
}
export function Split() {
useTrackDemoRender();
return (
<Counter>
<Heavy />
</Counter>
);
}なぜ止まるのか
鍵は「その <Heavy /> が、どの関数の中に書いてあるか」です。
<Heavy /> という JSX は、 Part 1 でやったとおりただの値です。 ここまではいいとして、その値がいつ作られるのかが問題になります。
関数の中に書いた行は、その関数が呼ばれるたびに、上から順に実行されます。 Part 6 で見た const options = { ... } が 毎回新しいオブジェクトになったのと、まったく同じことです。
つまり <Heavy /> も、Counter の中に書いてあれば、Counter が動くたびに作り直されます。外に書いてあれば、作り直されません。
- 中に直接書いた場合、
<Heavy />はCounter の中にある。 Counter が実行されるたびに、新しく作り直される - children として渡した場合、
<Heavy />はCounter の外にある。 Counter がいくら実行されても、そこは実行されない
Counter が描き直されるとき、React が受け取る children は前回とまったく同じものです。
split.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
return (
<RenderBox title="重い子" tone="highlight">
外で作られて、children として渡されている
</RenderBox>
);
}
// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count: {count}
</Button>
{/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
{children}
</div>
);
}
export function Split() {
useTrackDemoRender();
return (
<Counter>
<Heavy />
</Counter>
);
}どういうときに効くか
Part 7 で state を使う場所まで下ろしたのと同じ発想です。 違うのは、重い表示を入力と同じ階層から動かせないときにも 使えることです。
この形が効くのは、状態が頻繁に変わる場所と、重い表示が同じ階層にいるときです。
// 入力のたびに全部が描き直される
function Page() {
const [keyword, setKeyword] = useState("");
return (
<div>
<input value={keyword} onChange={...} />
<HeavyChart /> {/* 打つたびに描き直される */}
</div>
);
}検索欄に文字を打つたび、関係のないグラフまで描き直されます。 ここで「入力を持つ部分」を切り出し、 グラフを children として渡せば、巻き込まれなくなります。
function Page() {
return (
<SearchBox>
<HeavyChart /> {/* SearchBox の外で作られる */}
</SearchBox>
);
}split.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
return (
<RenderBox title="重い子" tone="highlight">
外で作られて、children として渡されている
</RenderBox>
);
}
// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count: {count}
</Button>
{/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
{children}
</div>
);
}
export function Split() {
useTrackDemoRender();
return (
<Counter>
<Heavy />
</Counter>
);
}理解できたか確かめる
確認クイズ
children として渡した子が描き直されないのはなぜ?
確認クイズ
この方法と memo、どちらを先に検討する?
split.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
return (
<RenderBox title="重い子" tone="highlight">
外で作られて、children として渡されている
</RenderBox>
);
}
// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count: {count}
</Button>
{/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
{children}
</div>
);
}
export function Split() {
useTrackDemoRender();
return (
<Counter>
<Heavy />
</Counter>
);
}この章のまとめ
- JSX は書かれた場所で作られる値
childrenとして渡した子は親の外で作られるので、 親が描き直されても巻き込まれないmemoを使わず、state を持つ範囲を狭くするだけで描き直しが減る- 判定はいつも「同じものかどうか」。 state 更新も memo もこの章も、すべて同じ仕組み
split.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
return (
<RenderBox title="重い子" tone="highlight">
外で作られて、children として渡されている
</RenderBox>
);
}
// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count: {count}
</Button>
{/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
{children}
</div>
);
}
export function Split() {
useTrackDemoRender();
return (
<Counter>
<Heavy />
</Counter>
);
}