ReactJS - Tối ưu hóa hiệu suất cho ứng dụng với useCallback

Dmitri Pavlutin
Đang cập nhật

Hello, chào các bạn. Ở bài viết trước mình đã đề cập đến chủ đề tối ưu hóa hiệu suất (Performance and optimization) cho ứng dụng React sử dụng React.memo(). Qua đó, chúng ta đã thấy được cách thức render của các component, với một component khi được bọc (wrapped) trong React.memo(), React sẽ render component đó và ghi lại giá trị của lần render này trong bộ nhớ của nó. Đến lần render tiếp theo, nếu như các thuộc tính mới (props) được truyền vào giống như thuộc tính đã được ghi nhớ, React sẽ tái sử dụng giá trị đã được ghi nhớ, bỏ qua component này và tiếp tục render các component khác. Tuy nhiên, khi ta truyền một callback function từ component cha (parent component) đến các component con (child component), thì React.memo() không giải quyết được. Trước khi đưa ra lý do, chúng ta cần biết trong javascript có 2 loại kiểu dữ liệu khác nhau:

  • Primitive: Đây là loại chứa các kiểu giá trị bất biến (immutable data types) bởi vì không có cách nào để thay đổi giá trị khi nó đã được tạo ra. Kiểu dữ liệu này gồm : number, string, boolean, null, undefined`. VD:

    let greeting = "Hello World";
    greeting[0] = "A"
    console.log(greeting) 
    //Output
    Hello World
  • Non-Primitive: gồm những kiểu dữ liệu có thể thay đổi được giá trị của nó sau khi được khởi tạo gồm các kiểu dữ liệu như : Object, Array, Function, Set... VD 1:

let billionaires = ["Elon Musk", "Jeff Bezos", "Warrent Buffet", "Donald Trump"];
billionaires[2] = "X";
console.log(billionaires)
//Output
 ["Elon Musk", "Jeff Bezos", "X", "Donald Trump"];

VD 2:

let arr1 = [];
let arr2 = [];
console.log(arr1 === arr2) //false khác địa chỉ vùng nhớ
console.log(arr1 === arr1) //true cùng địa chỉ vùng nhớ
console.log(arr2 === arr2) //true

VD3:

function factory(){
  return (a,b){
    return a + b ;
  }
}
s1 = factory();
s2 = factory();
s1 === s2 // false
s1 === s1 // true

s1 và s2 là 2 hàm tính tổng 2 số, chúng được tạo bởi hàm bậc cao hơn (higher order function) factory(). Hàm trong Javascript được coi như "một công dân hạng 1" (first-class citizens), có thể hiểu rằng hàm là một đối tượng (Object) rất phổ biến. Trong một hàm đối tượng (function object), chúng có thể dùng để return ra những hàm khác (giống như factory() làm ở trên), cũng có thể thực hiện tính toán các biểu thức... Nói đơn giản, chúng thể làm những gì mà ta muốn . Hai hàm s1, s2 có cùng code với nhau, nhưng chúng lại trỏ đến địa chỉ vùng nhớ khác nhau, nên khi so sánh 2 hàm, dù cùng giá trị với nhau nhưng chúng không bằng nhau. Đó là đặc điểm của Object, khi cùng địa chỉ vùng nhớ, cùng giá trị thì chúng mới bằng nhau s1 === s1

Quay trở lại với chủ đề chính của bài viết, React.memo() thực sự hoạt động hiệu quả khi ta truyền vào nó các props có kiểu dữ liệu Primitive khi đó các giá trị thuộc kiểu primitive sẽ được lưu vào bộ nhớ để lần sau khi chuẩn bị render, nó sẽ so sánh được với props mới để quyết định có re-render hay sử tái sử dụng props đã được lưu. Vậy khi truyền một callback function, tại sao React.memo lại không thể hoạt động được? Vì mỗi lần re-render, component cha sẽ khởi tạo một địa chỉ vùng nhớ mới cho hàm đối tượng (new function instance) để truyền vào props của component con, component con nhận địa chỉ vùng nhớ mới sẽ đem so sánh với địa chỉ vùng nhớ được lưu trước và chắc chắn nó không trùng khớp, vì thế React.memo sẽ luôn trả giá trị false, đồng nghĩa với việc re-render component. Và đó là lý do useCallback được sử dụng.

Khi nào nên sử dụng useCallback()?

Giả sử ta có danh sách 100 bài posts, tạo 2 file gồm App.jsPosts.js

Xét trường hợp component cha App.js render mỗi giây 1 lần :

// ./src/App.js

import "./styles.css";
import React from "react";
import posts from "./posts.json";
import Posts from "./components/Posts";

export default function App() {  
  const [_, setRender] = React.useState(false);
  React.useEffect(() => {
    let timer = setInterval(() => {
      setRender(prevState => !prevState)
    }, 1000)
    return () => clearInterval(timer)
  })
  const onClickItem = e => {
    console.log("Bạn đã click", e.currentTarget);
  }
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <Posts posts={posts} onClickItem={onClickItem}/>
    </div>
  );
}
// ./src/components/Posts.js

import React from "react"

const Posts = ({posts, onClickItem}) => {
  console.log("render")
  return (
    <div>
      {posts.map(post => (
        <div key={post.id} style={{border : "1px solid", marginBottom : "1rem"}} onClick={onClickItem}>
          <h4>{post.title}</h4>
          <p>{post.description}</p>
        </div>
      ))}
    </div>
  )
}
export default React.memo(Posts)

Cũng sử dụng React.memo() cho Posts.js*, sau đó mở console để kiểm tra nó xem Posts.js có console "render" hay không?

Rõ ràng ta đã sử dụng React.memo để ghi nhớ lại các props để khi component cha ở đây là App.js render, nó sẽ so sánh với props mới để đi đến quyết định có render hay không. Trong trường hợp này, các props truyền vào không thay đổi, vậy Posts.js vẫn render. Bây giờ, mình thử thay đổi một chút ở function onClickItem bằng cách thêm useCallback vào xem sao nhé.

// ./src/components/Posts.js
import "./styles.css";
import React from "react";
import posts from "./posts.json";
import Posts from "./components/Posts";

export default function App() {  
  const [_, setRender] = React.useState(false);
  React.useEffect(() => {
    let timer = setInterval(() => {
      setRender(prevState => !prevState)
    }, 1000)
    return () => clearInterval(timer)
  })
  const onClickItem = React.useCallback(e=> {
    console.log("Bạn đã click" , e.currentTarget);
  },[posts]) 
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <Posts posts={posts} onClickItem={onClickItem}/>
    </div>
  );
}

Sau khi mình thay đổi useCallback, mở console để xem Posts.js thì nó hiển thì đúng một lần render dù cho App.js có re-render bao nhiêu lần.

Hàm useCallback tham số thứ 2 nhận giá trị posts để đảm bảo rằng nếu giá trị posts không thay đổi thì React.memo vẫn lưu địa chỉ của object function onClickItem tương tự như lần render trước. Lúc này, các props truyền vào của lần trước so sánh với props truyền vào của lần tiếp theo sẽ chính xác, React.memo nhận diện được và so sánh chính xác.

Khi nào không nên sử dụng useCallback()?

Sử dụng useCallback() hợp lý sẽ giúp ứng dụng của bạn cải thiện hiệu suất, nhưng nếu sử dụng không hợp lý, nó có thể làm cho ứng dụng của bạn trở nên chậm hơn. Xét ví dụ sau đây :

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);

  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

Trong function handleClick sử dụng useCallback với tham số thứ 2 là một mảng rỗng nghĩa là mỗi lần MyComponent render thì useCallback này đều được gọi. Thậm chí useCallback về object function tương tự như trước khi re-render. Điều này thực sự không đem lại hiệu quả vì chi phí cho việc tối ưu còn cao hơn cả khi không có nó .

Vậy trong ví dụ này, đơn giản ta không cần gọi useCallback

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = () => {
    // handle the click event
  };

  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

Kết luận

Sử dụng các công cụ cho việc tối ưu hóa hiệu suất của một ứng dụng đem đến sử cải thiện đáng kể về hiệu suất. Trong một số trường hợp component con chưa lượng dữ liệu lớn cần phải sử dụng callback function để ghi nhớ là rất cần thiết. Tuy nhiên, bất kỳ sự tối ưu nào đều làm tăng thêm sự phức tạp ,vì thế nếu sử dụng không đúng, không hợp lý, nó sẽ làm cho hiệu suất ngày càng chậm hơn vì những đoạn code được tối ưu có thể thay đổi nhiều lần.

Vậy, trước khi bắt tay vào việc tối ưu hiệu suất của ứng dụng, bạn cần nắm 2 điều này : - "Hồ sơ lý lịch" của component (Lượng dữ liệu trong component có nhiều hay không, dữ liệu có thay đổi thường xuyên hay không ? ) - Định lượng hiệu suất có khả năng được cải thiện (vd : sau khi cải thiện, tốc độ render tăng từ 50ms đến 100ms)

Sau đó hãy tự hỏi : Có nên tăng hiệu suất hay không? Có nên đánh đổi giữa hiệu suất và độ phức tạp của thuật toán khi dùng hiệu suất không ? Liệu useCallback() có đáng để dùng trong trường hợp này không ?

Bài viết đến đây cũng đủ dài, mình xin tạm dừng tại đây, cám ơn các bạn đã theo dõi. Chúc các bạn thành công. -


Bài viết có liên quan