本文へスキップ
memo

Part 8 · パフォーマンス

41 / 64

memo

再レンダリングを止める

Part 7 で、親が描き直されると子も描き直されることを見ました。props が変わっていなくても、です。

たいていはそれで問題ありません。ですが、 子の中身が重かったり、数が多かったりすると効いてきます。

そこで登場するのが memo です。props が変わっていない子をスキップするための道具です。

包むだけで、描き直しが止まる

下のデモには子が 2 つあります。中身はまったく同じで、片方だけ memo で包んであります。

// 包んでいない
function Plain({ name }) { ... }

// 包んだだけ。中身は同じ
const Memoized = memo(function Memoized({ name }) { ... });

「count を増やす」を押してください。name は変わらないので、子の表示も変わりません。

memo あり / なしを並べる

count を押したときの、2 つの子の違いを見る

count: 0

memo なし
さとう さん
memo あり
さとう さん

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 の中身も変わりません。それでも押してみてください。

memo が効かない例うまくいかない例

包んであるのに、押すたびに光る

count: 0

memo あり(オブジェクトを受け取る)
さとう さん

光ります。原因はこの 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 の見比べが効いています。

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 でこのあと出てくる useMemouseCallback。 やっていることは共通しています。

前の結果をとっておいて、使い回す。これをまとめてメモ化と呼びます。 (メモを取っておく、の「メモ」です)

記事や求人票でこの言葉を見かけたら、この 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>
  );
}