iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
Modern Web

你 React 了嗎? 30 天解鎖 React 技能系列 第 18

[DAY 18] useMemo 緩存記憶體,避免重新渲染

  • 分享至 

  • xImage
  •  

[情境劇場]

解師傅:本來明天要請周J倫來表演,但他臨時有事,所以改請六月天樂團了!

小當家:哦!!那我要改一下了

解師傅:改什麼?

小當家:我的腦袋記錄名單裡,周J倫愛吃雞蛋豆腐,六月天裡面的團員有兩個不愛,因為現在變更名單,所以明天上的菜色將會做更動

解師傅:真有你的...


cover

認識 useMemo

useMemo 是一個 Memorized Hook,每當 render 時都會重新渲染(re-render)組件,如果遇到不必要渲染程式,重新渲染就會造成效能上的浪費,透過 useMemo 可以將函式運算完的值存一個記憶體(memoization),你也可以把它視為緩存值,以減少組件不必要的重新渲染


useMemo 使用方法

  • 從 react 中載入 useMemo 方法
  • useMemo 帶入參數,第一個參數為函式,第二個參數為 dependencies 陣列

1. 從 react 中載入 useMemo 方法

import { useMemo } from "react";

2. useMemo 帶入參數

const memoizedValue = useMemo(() => { return fn(a) }, [a]);

第一個參數為函式,函式回傳不必要重新渲染的程式

第二個參數為 dependencies 陣列,useMemo 會依 dependencies 陣列去做比對差異,如果 dependencies 有變動,才會重新渲染,並回傳一個值
如沒有 dependencies,則放空陣列,每次 render 時都會計算新的值。


useMemo 使用情境

  • 減少組件重新渲染大量複雜的計算
  • useEffect 的 dependencies 為物件或陣列

1. 減少組件重新渲染大量複雜的計算

複雜的計算本身就會吃比較多效能,每次狀態有變動又 re-render,但事實上我們只需要加上 dependencies 就可以減少 re-render 的次數

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(100);

  const changeCount = (e) => {
    setCount(value);
  };

  const evenNumbers = [];

  for (let i = 1; i <= count; i++) {
    if (i % 2 === 0 && String(i).includes("2")) {
      evenNumbers.push(i);
    }
  }

  return (
    <div>
      <h2>{count}</h2>
      <div>
        <input type="number" value={count} onChange={(e) => changeCount(e)} />
      </div>
      <div>1~{count} 為偶數且數字有 2 的號碼:</div>
      {evenNumbers.join(", ")}
    </div>
  );
}

usememo1
可以不用太仔細讀程式碼,這邊簡單說明一下程式範例目的:

  • 這是一個數字型態的 input
  • 輸入數字後便會計算出偶數且數字有 2 的號碼

打開 codesandbox 程式碼範例 看看!


這邊乍看之下沒什麼太大的問題,於是再加入了一個計時器進來

import { useState, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(50);

  const changeCount = (e) => {
    setCount(value);
  };

  const evenNumbers = [];

  for (let i = 1; i <= count; i++) {
    if (i % 2 === 0 && String(i).includes("2")) {
      evenNumbers.push(i);
    }

    console.log("render evenNumbers");
  }

  const time = useTime();

  function useTime() {
    const [time, setTime] = useState(0);

    useEffect(() => {
      let i = 0;

      const interval = setInterval(() => {
        setTime(i++);
        console.log("render time");
      }, 1000);

      return () => {
        clearInterval(interval);
      };
    }, []);

    return time;
  }

  return (
    <div>
      <span>time:{time}</span>
      <h2>{count}</h2>
      <div>
        <input type="number" value={count} onChange={(e) => changeCount(e)} />
      </div>
      <div>1~{count} 為偶數且數字有 2 的號碼:</div>
      {evenNumbers.join(", ")}
    </div>
  );
}

usememo2

加了計時器後,每 1 秒計時器會 +1

為了方便觀察,我們在計時器和 evenNumbers 的函式分別下了 console.log ,每當計時器變動,會發現 evenNumbers 的地方又重新渲染了一次!

由此可知,只要有元素狀態變更,整個都會重新 render 一次,這造成了效能上的浪費

打開 codesandbox 程式碼範例 看看



這時候就可以用 useMemo 解救這個問題!

const evenNumbers = useMemo(() => {
	const result = [];
	for (let i = 1; i <= count; i++) {
	  if (i % 2 === 0 && String(i).includes("2")) {
	    result.push(i);
	  }
	  console.log("evenNumbers");
	}
	return result;
}, [count]);

usememo3

我們用了 useMemo ,讓 evenNumbers 只在 count 有變動時才會渲染,現在可以看到,即使計時器一直變動,evenNumbers 也不會再重新 render 囉!

打開 codesandbox 程式碼範例 看看


2. useEffect 的 dependencies 為物件或陣列

在 JavaScript 用相同的物件或陣列做比對時,你會發現

{} === {} // false
[] === [] // false

因為 objectArray 非基本型態,比較都是 by reference,雖然內容一樣,但實際上是不一樣的 object、Array,所以如果我們把 object、Array 當作 dependencies,還是會每次都再重新渲染一次,這樣是沒有意義的


[範例]

import { useEffect, useState } from "react";

function App() {
  const [state, setState] = useState(true);
  const [inputValue, setInputValue] = useState("");

  const style = {
    backgroundColor: state ? "black" : "yellow",
    width: "100px",
    height: "100px",
		margin: "auto"
  };

  useEffect(() => {
    console.log("Change Color");
  }, [style]);

  return (
    <div>
      <div style={style}></div>

      <input
        value={inputValue}
        onChange={(e) => {
          setInputValue(e.target.value);
        }}
        placeholder="輸入文字"
      />

      <button
        onClick={() => {
          setState((state) => !state);
        }}
      >
        Change Color
      </button>
    </div>
  );
}

export default App;

usememo

簡單說明一下情境:

  • 這邊有一個輸入文字框
  • 跟換顏色的按鈕
  • 預期顏色變換後才執行 side effect (style 變更將印出 "Change Color")

打開 codesandbox 程式碼範例 看看



目前只有一開始執行的 useEffect,看起來沒什麼大問題

試著輸入文字將會發現

usememo


還是執行了 side effect!!/images/emoticon/emoticon04.gif


這就是因為物件或陣列即使做了比對,但因為是 by reference,所以還是會被認定為不一樣,進而呼叫 side effect

我們只要用 useMemo 包起來,就可以解決這個問題

const style = useMemo(() => {
  return {
    backgroundColor: state ? "black" : "yellow",
    width: "100px",
    height: "100px",
    margin: "auto"
  };
}, [state]);

usememo

現在即使輸入了文字,也不會再執行 side effect 囉!

打開 codesandbox 程式碼範例 看看吧!


其實遇到這種情況,eslint 也會跳出建議訊息,防止開發者沒注意到這個問題

The 'style' object makes the dependencies of useEffect Hook (at line 17) change on every render. To fix this, wrap the initialization of 'style' in its own useMemo() Hook. (react-hooks/exhaustive-deps)eslint

結語

useMemo 替我們節省了很多效能,但也不要濫用 useMemo ,過多的記憶體,也是會造成效能上的問題,所以我們要用在適當的情境使用,如會頻繁的渲染就較不適合,最好是用在執行速度很慢、變動性不大的函式,以減少重新渲染的目的。


Reference:

Understanding useMemo and useCallback
How To Use Memoization To Drastically Increase React Performance


本文將同步更新至我的部落格
Lala 的前端大補帖



上一篇
[DAY 17] useEffect 處理副作用
下一篇
[DAY 19] useCallback 函式記憶體
系列文
你 React 了嗎? 30 天解鎖 React 技能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言