Part 8 · パフォーマンス
41 / 64 章
memo
再レンダリングを止める
Part 7 で、親が描き直されると子も描き直されることを見ました。props が変わっていなくても、です。
たいていはそれで問題ありません。ですが、 子の中身が重かったり、数が多かったりすると効いてきます。
そこで登場するのが memo です。props が変わっていない子をスキップするための道具です。
包むだけで、描き直しが止まる
下のデモには子が 2 つあります。中身はまったく同じで、片方だけ memo で包んであります。
// 包んでいない
function Plain({ name }) { ... }
// 包んだだけ。中身は同じ
const Memoized = memo(function Memoized({ name }) { ... });「count を増やす」を押してください。name は変わらないので、子の表示も変わりません。
count を押したときの、2 つの子の違いを見る
count: 0
memo なしの子だけが光り、回数が増えていきます。memo ありの子は静かなままです。
「name を変える」を押すと、今度は両方が光ります。 props が実際に変わったので、memo でもスキップできません。
memo-demo.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// メモ化していない子
function Plain({ name }: { name: string }) {
return <RenderBox title="memo なし">{name} さん</RenderBox>;
}
// 中身はまったく同じ。memo で包んだだけ
const Memoized = memo(function Memoized({ name }: { name: string }) {
return (
<RenderBox title="memo あり" tone="highlight">
{name} さん
</RenderBox>
);
});
const names = ["さとう", "すずき", "たかはし"];
export function MemoDemo() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [name, setName] = useState(names[0]);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(name は変わらない)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName(names[(names.indexOf(name) + 1) % names.length])}
>
name を変える
</Button>
</div>
<div className="flex flex-col gap-2.5">
<Plain name={name} />
<Memoized name={name} />
</div>
</div>
);
}包んだのに効かない
ここからが本題です。memo は、包めば必ず効くわけではありません。
下のデモも memo で包んであります。user の中身も変わりません。それでも押してみてください。
包んであるのに、押すたびに光る
count: 0
光ります。原因はこの 1 行です。
const user = { name: "さとう" };Part 6「無限ループにしない」で見たとおり、この行は描き直されるたびに実行され、毎回新しいオブジェクトを作ります。 中身は同じでも、React にとっては別のものです。
memo は props を見比べるとき、「同じものかどうか」だけを見ます。 Part 4 の「オブジェクトと配列の更新」 の話と、まったく同じ判定です。 毎回別のものが渡ってくるので、「props が変わった」と判断されます。
memo-broken.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// さっきと同じように memo で包んである
const Memoized = memo(function Memoized({ user }: { user: { name: string } }) {
return (
<RenderBox title="memo あり(オブジェクトを受け取る)" tone="highlight">
{user.name} さん
</RenderBox>
);
});
export function MemoBroken() {
useTrackDemoRender();
const [count, setCount] = useState(0);
// 描き直されるたびに、新しいオブジェクトが作られる
const user = { name: "さとう" };
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(user の中身は変わらない)
</Button>
<Memoized user={user} />
</div>
);
}いちばん簡単な直し方
原因は「毎回作り直していること」でした。 なら、作り直さなければいい。 この user は誰にも依存していないので、関数の外に出すだけで済みます。
// ✕ 描き直されるたびに、新しいオブジェクト
function MemoBroken() {
const user = { name: "さとう" };
...
}
// ○ 一度だけ作られて、ずっと同じもの
const user = { name: "さとう" };
function MemoFixed() {
...
}何度押しても、下の箱は光らない
count: 0
光らなくなりました。渡しているものが毎回同じなので、memo の見比べが効いています。
memo-fixed.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
const Memoized = memo(function Memoized({ user }: { user: { name: string } }) {
return (
<RenderBox title="memo あり(オブジェクトを受け取る)" tone="highlight">
{user.name} さん
</RenderBox>
);
});
// 描き直しのたびに作り直す必要がない。外に出せば、ずっと同じもの
const user = { name: "さとう" };
export function MemoFixed() {
useTrackDemoRender();
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(user の中身は変わらない)
</Button>
<Memoized user={user} />
</div>
);
}いつ使うか
ここまで読むと、全部 memo で包みたくなるかもしれません。やめてください。
memo はタダではありません。 props を見比べる処理そのものにコストがかかります。軽い子を包むと、比較のぶんだけ遅くなります。
そして読みにくくなります。
まだ遅くもないのに包むと、読みにくさは必ず増えて、速さは増えるか分かりません。 割に合いません。
それに、次の章でやる方法を使えば、memo を使わずに描き直しを減らせる場合があります。
memo-demo.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// メモ化していない子
function Plain({ name }: { name: string }) {
return <RenderBox title="memo なし">{name} さん</RenderBox>;
}
// 中身はまったく同じ。memo で包んだだけ
const Memoized = memo(function Memoized({ name }: { name: string }) {
return (
<RenderBox title="memo あり" tone="highlight">
{name} さん
</RenderBox>
);
});
const names = ["さとう", "すずき", "たかはし"];
export function MemoDemo() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [name, setName] = useState(names[0]);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(name は変わらない)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName(names[(names.indexOf(name) + 1) % names.length])}
>
name を変える
</Button>
</div>
<div className="flex flex-col gap-2.5">
<Plain name={name} />
<Memoized name={name} />
</div>
</div>
);
}この手のやり方を「メモ化」と呼びます
memo、そしてこの Part でこのあと出てくる useMemo と useCallback。 やっていることは共通しています。
前の結果をとっておいて、使い回す。これをまとめてメモ化と呼びます。 (メモを取っておく、の「メモ」です)
記事や求人票でこの言葉を見かけたら、この 3 つのことだと思ってください。
memo-demo.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// メモ化していない子
function Plain({ name }: { name: string }) {
return <RenderBox title="memo なし">{name} さん</RenderBox>;
}
// 中身はまったく同じ。memo で包んだだけ
const Memoized = memo(function Memoized({ name }: { name: string }) {
return (
<RenderBox title="memo あり" tone="highlight">
{name} さん
</RenderBox>
);
});
const names = ["さとう", "すずき", "たかはし"];
export function MemoDemo() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [name, setName] = useState(names[0]);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(name は変わらない)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName(names[(names.indexOf(name) + 1) % names.length])}
>
name を変える
</Button>
</div>
<div className="flex flex-col gap-2.5">
<Plain name={name} />
<Memoized name={name} />
</div>
</div>
);
}理解できたか確かめる
確認クイズ
memo で正しく包んだのに、子が描き直される。原因は?
確認クイズ
memo はどこに使うべき?
memo-broken.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// さっきと同じように memo で包んである
const Memoized = memo(function Memoized({ user }: { user: { name: string } }) {
return (
<RenderBox title="memo あり(オブジェクトを受け取る)" tone="highlight">
{user.name} さん
</RenderBox>
);
});
export function MemoBroken() {
useTrackDemoRender();
const [count, setCount] = useState(0);
// 描き直されるたびに、新しいオブジェクトが作られる
const user = { name: "さとう" };
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(user の中身は変わらない)
</Button>
<Memoized user={user} />
</div>
);
}この章のまとめ
memoは、props が変わっていなければ子の描き直しをスキップする- 見比べ方は「同じものかどうか」。中身は見ない
- オブジェクト・配列・関数を渡すと、 毎回新しく作られるため memo は効かない
- 先回りして使わない。遅いと分かってから、重い箇所に絞る
memo-demo.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// メモ化していない子
function Plain({ name }: { name: string }) {
return <RenderBox title="memo なし">{name} さん</RenderBox>;
}
// 中身はまったく同じ。memo で包んだだけ
const Memoized = memo(function Memoized({ name }: { name: string }) {
return (
<RenderBox title="memo あり" tone="highlight">
{name} さん
</RenderBox>
);
});
const names = ["さとう", "すずき", "たかはし"];
export function MemoDemo() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [name, setName] = useState(names[0]);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">count: {count}</p>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(name は変わらない)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setName(names[(names.indexOf(name) + 1) % names.length])}
>
name を変える
</Button>
</div>
<div className="flex flex-col gap-2.5">
<Plain name={name} />
<Memoized name={name} />
</div>
</div>
);
}