React Hooks Performance 效能處理

React Hooks Performance 效能處理

June 01, 2019

·

10 min read

React 發布了幾個月的 Hooks,最近也開始嘗試接觸,後面會稍微提一下 PureComponent,不會介紹 hooks 各種特殊用法,就只針對 hooks performance 優化做介紹,還有搭配 redux 的處理。

因為前陣子有處理過 React 優化效能,對於這件事情也開始在意,讓人絕望的 google page speed…。

React PureComponent

如果有在注意效能的話,你應該會聽過 shouldComponentUpdate 或 PureComponent,這是較常見的處理方法,Purecomponent 只關注 state、props 並作 shallow equal 比較,當不同值才會觸發 rerender。

React shallow-compare

以下是有無使用 Purecomponent 的比較,當我更新某個 state,而這個 state 沒有傳進作為 props,PureComponent 會過濾掉更新。

使用教學: 你可以嘗試更新 input text,會發現 Purecomponent 數字不會增加,而一般 component 則是會增加。這數字增加代表著 react 嘗試 update Component 次數。

這數字不代表是否真的更新 dom,因為 react 會比較 render 後 dom 結構的不同,再決定是否更新某節點 dom,但嚴格來說這也算是種浪費效能。

  • PureComponent code
import React, { PureComponent } from "react";

class CheckboxPure extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      done: true,
    };
    this.times = 0;
  }
  changeCheck = (e) => {
    this.setState({
      done: e.target.checked,
    });
  };
  componentWillUpdate() {
    this.times = this.times + 1;
  }
  render() {
    const { done } = this.state;
    const { text } = this.props;
    return (
      <div>
        <div>PureComponent component Try Update time {this.times}</div>
        <div>
          <input onClick={this.changeCheck} type="checkbox" checked={done} />
          {text}
        </div>
      </div>
    );
  }
}

export default CheckboxPure;

ps. PureComponent 不是全部都用,需要注意 props 的更新關係。假設你上層的 update,一定會更動到 PureCompoent 的 props,那你應該避免使用 PureComponent,因為每次接受到 props 時,PureComponent 還會多做一次 shallow compare,那因為每次都一定更新 props,多做比較就等於浪費效能,比起用一般方法還不好。

React hooks functional

前面會提到 PureComponent,是因為 react hooks 是全面的使用 functional Component,這代表我們不會在使用 Class,以往 Class 使用是繼承 React 並讓我們建立 instance,有 instance 就代表有 memory 位置,可以讓我們處理資料比較。functional 代表我們只要調用一次更新,所有的 react hooks function 都會再被調用一次。

舉例來說,將關注點變到更小,所以 useEffect 才能實現像是 componentDidUpdate 的功能。

  • Hooks like componentDidUpdate
useEffect(() => {
  document.title = `You clicked ${count} times`;
};
// it will setting title everytime when render function

useEffect 就是一個例子,你看到 useEffect 的額外第二個參數,useEffect 會綁定 count 更新,才會調用 callback。

  • Hooks useEffect bind count
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);
// Only setting title if count changes

React hooks 實現 todoList

嘗試建立一個 toDo List,方便我們來看怎樣讓 React hooks 實現 PureComponent 的特性。我們會需要建立三個檔案,分別是是 container/todoView、component/todoLis、hooks/useTodoList。

假設你已經用過 react hooks,這部分可以直接略過。

首先建立 container/TodoView,我們會需要建立 toDo 的 Array,這邊我們會用到 useState,還有 useRef,讓我們能夠取得 input value,剩下部份就是更新處理 todoList state。

  • React hooks function
// toDo array
const [todoList, setTodoList] = useState([]);

// create inputRef
const inputEl = useRef(null);

// add Array
const addTodo = (event) => {
  event.preventDefault();
  if (!inputEl.current.value) {
    return;
  }
  const mergeArr = [...todoList, inputEl.current.value];
  inputEl.current.value = "";
  return setTodoList(mergeArr);
};

// delete Array by index
const deleteToDo = (index) => {
  const newArr = [...todoList];
  newArr.splice(index, 1);
  return setTodoList(newArr);
};

建立 hooks/useTodoList,並把上面這些 hooks function 移動過去。就完成了 todoList 的自製 hooks。

  • hooks/useTodoList.js
import { useState, useCallback } from "react";

function useTodoList(value, inputEl) {
  const [todoList, setTodoList] = useState(value);
  const addTodo = (event) => {
    event.preventDefault();
    if (!inputEl.current.value) {
      return;
    }
    const mergeArr = [...todoList, inputEl.current.value];
    inputEl.current.value = "";
    return setTodoList(mergeArr);
  };

  const deleteToDo = (index) => {
    const newArr = [...todoList];
    newArr.splice(index, 1);
    return setTodoList(newArr);
  };

  return [todoList, addTodo, deleteToDo];
}

export default useTodoList;

會多建立一個 const [count, setCount] = useState(0);,讓我們在這層 setState,並觀察 TodoList 更新狀況。

  • container/TodoView.js
import React, { useState, useRef } from "react";
import TodoList from "../component/TodoList";
import useTodoList from "../hooks/useTodoList";

function TodoView() {
  const inputEl = useRef(null);
  const [todoList, addTodo, deleteToDo] = useTodoList([], inputEl);

  // use to update TodoView
  // let us check TodoList update situation
  const [count, setCount] = useState(0);

  return (
    <>
      <span>Counter : {count}</span>
      <button onClick={() => setCount(count + 1)}>Add Counter</button>
      <form className="input-container" onSubmit={addTodo}>
        <input ref={inputEl} placeholder="Type your to Do" />
        <button className="add-button">Create</button>
      </form>
      <TodoList todoList={todoList} deleteToDo={deleteToDo} />
    </>
  );
}

export default TodoView;

額外再加上 toDoList.js 加上計算器,每次的 render function 都會加上 1,方便我們看 toDoList 重新 render 的次數。

  • component/TodoList.js
import React from "react";
let count = 0;

function TodoList(props) {
  const { todoList, deleteToDo } = props;
  count = count + 1;
  return (
    <div className="list">
      TodoList render Times {count}
      {todoList.map((value, index) => (
        <li className="list-item" key={`to_${index}`}>
          <div>
            {index + 1}. {value}
          </div>
          <span onClick={() => deleteToDo(index)}>-</span>
        </li>
      ))}
    </div>
  );
}

export default TodoList;

React hooks 效能處理

我們已經完成了簡易版的 todoList,當你輸入 input 建立後,會發現 TodoList 會更新一次,但是你點擊 count 後,會發現 TodoList 居然也會更新,這是因為所有的 component 已經都是純 functional component,當我們最上層更新 state,都會一路往下更新到底層。

這時候我們就必須依賴 React.memo,React.memo 是一個 high Order Component,功能就像是 PureComponent,讓我們擋住調用更新 function,但差異在於 memo 是用在於 function components,並會幫我們 memory 住 props,只在 props 更新才會往下更新。

React memo

  • component/TodoList
// use React memo for TodoList;
export default React.memo(TodoList);

更新上去後,讓我們在嘗試點擊 count,觀察 TodoList 是否就卡住更新了。

你會發現數字還是增加。

查看上層傳進的 props 後,發現還有一個問題,就是傳進去的 function,每次都會是一個新的 function。因為沒有 function 沒有 memory 住,導致每次都會 render 後都會重新建立 addTodo、deleteToDo,所以對 toDoList 的 memo 來說,你每次都給我新的 props function,當然會每次都更新 component。

幸好 react hooks 有提供 useCallback,讓我們可以把 function memory 起來,useCallback 會需要依賴第二個參數,讓他比較判斷是否要更新 function。

  • React hooks useCallback
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

React hooks usecallback

const addTodo = useCallback(
  (event) => {
    event.preventDefault();
    if (!inputEl.current.value) {
      return;
    }
    const mergeArr = [...todoList, inputEl.current.value];
    inputEl.current.value = "";
    return setTodoList(mergeArr);
  },
  [todoList, inputEl]
);

const deleteToDo = useCallback(
  (index) => {
    const newArr = [...todoList];
    newArr.splice(index, 1);
    return setTodoList(newArr);
  },
  [todoList]
);

更新上去後,再嘗試點擊 count 看看,會發現 toDoList 終於沒有更新數字了。這樣就完成了 hooks 的 render 效能處理。使用 React.memo 實現了類似 PureComponent 的功能,再解決掉 function components 沒有 memory 的問題,讓我們 todo、delete function,都不會因為 function component 被更新而重新被建立。

增加 Redux

另外改用 redux 管理 todo 資料,沒有特別用最新 react-redux 的 hooks 版本,因為還在 alpha 階段。基本上就移除掉 useState,建立 store、reducer,再建立 Provider,還有 state、dispatch 傳遞到需要使用的元件上。

不想偏離主題就直接貼上作法了。

Source code: React hooks with redux

心得

因為準備要開始運用 hooks 在專案上,才發現 function components 要注意的問題,遠比我想像的還多。以往 react class 的寫法,react 處理了 component 的 rerender 問題,但改為 function components 後,多了處理 rerender 的問題。

個人覺得用過 class 在轉用 hooks 後,lifeCycle 的部分最不習慣,感覺拉高了點 React 的學習門檻。hooks 讓 react 的複用單位拉到在更小,用得好確實能夠加速開發,期待日後實際運用 hooks 在專案上後能有更多心得分享。