【デイトラ学習記録】DAY35 パフォーマンスチューニング(useMemoとuseCallback)

今回は、パフォーマンスチューニングについて学習していきます。

パフォーマンスチューニングの概念

パフォーマンスとは

Webにおけるパフォーマンスとは、サイトの表示速度や、ユーザーのアクションの反応

チューニングとは

パフォーマンスをよくすること、最適化すること

Reactにおけるパフォーマンスチューニングの概念

→主に、コンポーネントが再レンダーされるタイミングをなるべく少なくすること

Reactで、コンポーネントが再レンダーされるタイミングとは

  • Propsの変更
    • 親コンポーネントから渡されるPropsが変更された場合、子コンポーネントが再レンダーされる
  • ステート(State) の変更
    • コンポーネント内で管理されているステートが変更された場合、そのコンポーネントは再レンダーされる。これは、setStateによってステートが更新されると、再レンダーがトリガーされる仕様のため
  • コンテキストの変更
    • コンポーネントがuseContextを使っていると、 使用してるコンテキストの値が変更されると、そのコンポーネントは再レンダーされる。コンテキストプロバイダーが新しい値を提供すると、コンテキストの値を利用している全てのコンポーネントが再レンダーされる
  • 親コンポーネントの再レンダー
    • 親コンポーネントが再レンダーされると、その子コンポーネントも再レンダーされる可能性がある。たとえプロパティやステートに変更がなくても、親が再レンダーされると、そのツリー全体が再レンダーされることがある、

→ こう考えると、基本的にほぼほぼ再レンダーされるように思うが悪いことではない。
元々Reactでは再レンダすることをチューニングされているので、そこまで気にしなくていい。
もっと大規模なたくさんコンポーネントを使ったアプリでは大事になってくる。

→ 何かブラウザの動きがもさっとしてる。。。 ブラウザのメモリを大幅に食っているなどのタイミングで初めて考えればいいこと。

TODOアプリの各コンポーネントにconsol.cogを仕込んで、再レンダーのタイミングを確認する。

再レンダーのタイミングを確認する

// 画像が入る

視覚的にコンポーネントの再レンダーを確認する

「Highlight updates when components render」をOFFにすることで、どのコンポーネントが再レンダーされているか視覚的に認識できる。

// 画像

TODOアプリをいじってみて再レンダーされているタイミングはある程度理解することができました。この規模では全然問題ないが、不要な再レンダーを防ぐ方法をみていきます

不要な再レンダーを防ぐパフォーマンスチューング

memo()

propsが変更されない限り、コンポーネントは再レンダーされない。

TSX
// MyComponent というコンポーネントがあった場合
const MyComponent = ({title, description} : Props ) => {
  // コンポーネントの内容
};

// そのコンポーネントをmemo() で囲う
const MyComponent = memo(({title, description} : Props ) => {
  // コンポーネントの内容
});

アプリの挙動を確認し、変更されていないのに再レンダーされている部分を調査する。
→ その部分はチューニングできる可能性がある。

TSX
const MyComponent = memo(({title, description, handleClick} : Props ) => {
  // コンポーネントの内容
});

useCallback

関数を子コンポーネントに渡す場合、その関数が再生成されると子コンポーネントも再レンダーされる。
useCallback を使って関数をメモ化し、依存関係が変更されない限り同じ関数インスタンスを維持することで、不要な再レンダーを防ぐ

TSX
const handleClick = useCallback(() => {
  // ハンドルの内容
}, [依存関係])

const MyComponent = memo(({title, description, handleClick} : Props ) => {
  // コンポーネントの内容
});

TODOを削除する関数をmemo化する。マウント時にmemo化すればOKなので、依存関係は空。

TSX
// TODOを削除する関数
const handleDelete = useCallback(
  (id: number) => setTodoList((prev) => prev.filter((todo) => todo.id !== id)),
  []
);

仮に、引数以外で、外部の変数の値などを使う場合には、依存関係に値を入れておく

TSX
const [todoList, setTodoList] = useState<Todo[]>([]);

// TODOを削除する関数
const handleDelete = useCallback(
  (id: number) => setTodoList((prev) => todoList.filter((todo) => todo.id !== id)),
  [todoList]
);

useMemo

計算コストの高い計算結果やオブジェクト、配列などをメモ化することで、依存関係が変更されない限り再計算を防ぐ。これにより、不要な再レンダーを防ぐ。

※依存関係が更新されて初めて初めて、useMemo の中身が実行される。
依存関係の指定によってバグの原因になるので注意が必要

TSX
const memoizedValue = useMemo(() => {
  return 重い処理;
}, [依存関係]);

Object.isの概念

Reactでは、比較にObject.is()の概念を使用している。
Object.is() とは、以下の仕様。

JavaScriptの概念

TSX
const obj1 = { a : 1}
const obj2 = { a : 1}
console.log(object.is(obj1, obj2));
// false
TSX
const obj1 = { a : 1};
const obj2 = obj1;
console.log(object.is(obj1, obj2));
// true
TSX
const str1 = "Hellow"
const str2 = "Hellow"
console.log(object.is(obj1, obj2));
// true

TodoListの再レンダーを防ぐ

TodoListのコンポーネントでは、filter されたObjectが使用されている。
filter は別のObjectとして生成している。

filter() は Array インスタンスのメソッドで、指定された配列の中から指定された関数で実装されているテストに合格した要素だけを抽出したシャローコピーを作成します。

memoやuseMemoでは、Propsの比較や、依存関係の比較には、内部的にObject.is() 利用されている。
なので、Objectの参照先が同じかを比較されているため、場合によってmemoやuseMemoで意図したチューニングにならないことがある。

それを解消するために、memoの第二引数に、比較条件をカスタムすることが可能

Objectの参照先が同じかを比較ではなく、Objectの中身が同じかを比較する関数を追加する必要がある。

JavaScript
const person1 = {
    "firstName": "John",
    "lastName": "Doe",
    "age": 35 
};

const person2 = {
    "firstName": "John",
    "lastName": "Doe",
    "age": 35,
};
 
const isDeepEqual = (object1, object2) => {

  const objKeys1 = Object.keys(object1);
  const objKeys2 = Object.keys(object2);

  if (objKeys1.length !== objKeys2.length) return false;

  for (var key of objKeys1) {
    const value1 = object1[key];
    const value2 = object2[key];

    const isObjects = isObject(value1) && isObject(value2);

    if ((isObjects && !isDeepEqual(value1, value2)) ||
      (!isObjects && value1 !== value2)
    ) {
      return false;
    }
  }
  return true;
};

const isObject = (object) => {
  return object != null && typeof object === "object";
};

console.log(isDeepEqual(person1, person2)); //true

上記の関数をmemoの第二引数を渡す

こうすることで、Objectの中身を比較するため、TodoListの再レンダーを防ぐことができる。

TSX
const isDeepEqual = (object1: any, object2: any) => {
  const objKeys1 = Object.keys(object1);
  const objKeys2 = Object.keys(object2);

  if (objKeys1.length !== objKeys2.length) return false;

  for (var key of objKeys1) {
    const value1 = object1[key];
    const value2 = object2[key];

    const isObjects = isObject(value1) && isObject(value2);

    if ((isObjects && !isDeepEqual(value1, value2)) || (!isObjects && value1 !== value2)) {
      return false;
    }
  }
  return true;
};

const isObject = (object: any) => {
  return object != null && typeof object === "object";
};

export const TodoList = memo(({ todoList, handleDelete }: Props) => {
  console.log("TodoList");
  return (
    <ul>
      {todoList.map((todo) => (
        <TodoItem
          id={todo.id}
          key={todo.id}
          task={todo.task}
          person={todo.person}
          deadline={todo.deadline}
          handleDelete={handleDelete}
        ></TodoItem>
      ))}
    </ul>
  );
}, isDeepEqual);

パフォーマンスチューニングにはReactの動きを知る必要がある

パフォーマンスチューニングするには、Reactの動きを知っておく必要がある。

  • Object.isの概念などの細かいところまで

一度パフォーマンスチューニングを頑張ってみることで、Reactの動きの部分まで学べる

パフォーマンスチューニングは必須?

結論必須ではない。問題が起きた時に実施すればOK

どちからといえばコードの可読性などの方が優先される

開発リポジトリ

https://github.com/yuta0824/dev-react-practice/tree/performance-tuning-day35

感想

パフォーマンスチューニングの概念について学習しました。
まだ、理解半分な内容が多いので、一度チューニングを自走して深掘りする必要がありそう。
Reactの内部的な仕様まで深く知る必要がある(チューニングすることで知ることができる)と思うので、腰を据えて復習したい。

Yuta | Code.Yu

WordPressをメインに活動する、フリーランスのWeb制作コーダーです。
React案件を経験したことをきっかけに、さらにフロントエンド開発のスキルを高めるため、JavaScriptやReactの学習を進めています。このブログでは、学習の過程や記録を発信しています。

Web制作に関する情報はこちら
Code.Yu | ホームページ制作・コーディング代行 ↗︎