// https://codesandbox.io/s/react-codesandbox-forked-ioy85?file=/src/index.js
import * as React from 'react'
import ReactDOM from 'react-dom'
function Log({ label }) {
console.log(`${label} render`);
return null;
}
function Counter() {
const [count, setCount] = React.useState(0);
return (
<React.Fragment>
<button onClick={() => setCount(c => c + 1)}>Increment {count}</button>
<Log label="counter" />
</React.Fragment>
)
}
Khi đoạn code trên được chạy, “counter render”
sẽ dược gọi và hiển thị trên mục console
với mỗi lần ta ấn Increment
. Điều này xảy ra bởi vì mỗi khi nút Increment
được ấn, trạng thái count
sẽ bị thay đổi và React cần làm mới phần tử để hiển thị dữ liệu đã được thay đổi khi ta cập nhật trạng thái count
với setCount
. Đồng nghĩa với việc, khi nhận được các phần tử mới đó, nó sẽ hiển thị và chuyển chúng vào DOM.
Đây là nơi mọi thứ trở nên thú vị. Chúng ta sẽ xem xét thực tế rằng <Log label="...." />
sẽ không bao giờ thay đổi giữa các lần kết xuất lại của React. Nó cố định và không hề thay đổi. Bây giờ chúng ta hãy làm lại vấn đề trên với một cách triển khai tốt hơn?
import * as React from 'react'
import ReactDOM from 'react-dom'
function Log({ label }) {
console.log(`${label} render`);
return null;
}
function Counter({ logger, ...props }) {
const [count, setCount] = React.useState(0);
return (
<React.Fragment>
<button onClick={() => setCount(c => c + 1)}>Increment {count}</button>
{ logger }
</React.Fragment>
)
}
....
ReactDOM.render(<Counter logger={<Log label="Counter" />} />, ...);
....
Mọi thứ được cải thiện hơn đúng không?. Chúng ta có được lần hiển thị Counter render
đầu tiên và điều bất ngờ là chúng k xuất hiện lại khi chúng ta ấn Increment
sau đó?.
Chuyện gì đang diễn ra?
Vậy điều gì gây ra sự khác biệt này?. Nó liên quan đến các phần tử React. Tại sao bạn không nghỉ ngơi nhanh chóng đọc thêm về “What is JSX” trước khi chúng ta tiếp tục để biết thêm về mối liên hệ giữa những điều trên hoặc cứ tiếp tục nhé.
Khi React gọi <Counter />
chúng ta sẽ nhận được một thứ tương tự như sau:
const Counter = {
type: "div",
props: {
children: [
{
type: 'button',
props: {
onClick: increment, // this is the click handler function
children: 'Increment 0',
},
},
{
type: Log, // this is our logger component function
props: {
label: 'counter',
},
},
],
}
}
Chúng được gọi là các đối tượng mô tả giao diện người dùng. Chúng mô tả giao diện người dùng mà React sẽ tạo ra trên DOM. Tiếp theo, khi chúng ta ấn click
vào <button>
các thay đổi sẽ hoạt động
const Counter = {
type: "div",
props: {
children: [
{
type: 'button',
props: {
onClick: increment, // [+] changes
children: 'Increment 1', // [+] changes
},
},
{
type: Log,
props: {
label: 'counter',
},
},
],
}
}
Có thể nói, những thay đổi duy nhất xảy ra ở đây là sự kiện onClick
và props children
khi chúng ta thay đổi trạng thái. Tuy nhiên, toàn bộ điều này hoàn toàn mới, kể từ lần đầu tiên sử dụng React, chúng ta đã tạo ra những đối tượng này hoàn toàn mới trên mỗi lần kết xuất (May mắn rằng, các trình duyệt dành cho thiết bị di động cũng khá nhanh, vì vậy đó chưa bao giờ là một vấn đề về hiệu suất đáng kể, có thể coi là thế).
Thực sự có thể dễ dàng hơn khi biết được các phần tử trong Tree React Elements
giữa mỗi lần hiển thị, ta có thể thấy được những phần tử “Không” thay đổi giữa các lần kết xuất lại đó.
const Counter = {
type: "div", // [---] keep
props: {
children: [
{
type: 'button', // [---] keep
props: {
onClick: increment,s
children: 'Increment 1',
},
},
{
type: Log, // [---] keep
props: {
label: 'counter', // [---] keep
},
},
],
}
}
Tất cả các loại phần tử đều giống nhau và điển hình là label
props trong phần tử <Log />
không hề thay đổi. Tuy nhiên, bản thân của mỗi props
sẽ thay đổi sau mỗi lần kết xuất lại. Mặc dù các thuộc tính của new props
giống nhau so với những các thuộc tính của props
trước đó.
Chúng ta có thể nhận ra điểm mấu chốt ở đây rằng. Bởi vì khi giá trị props
của <Log />
thay đổi, React sẽ cần gọi lại hàm Log
để đảm bảo rằng nó không nhận được bất kỳ JSX mới nào dựa trên props
mới. Nhưng điều gì sẽ xảy ra nếu chúng ta có thể ngăn các props
thay đổi giữa các lần hiển thị? Nếu props
không thay đổi, thì React sẽ biết rằng chúng không ảnh hưởng và không cần thiết phải gọi lại và thay đổi cấu trúc, thông tin JSX. Đây chính xác là những gì React đã được viết tại đây và theo cách đó kể từ khi React lần đầu tiên ra đời.
Nhưng đây cũng là vấn đề là **React sẽ tạo ra một nhánh props
mới mỗi lần chúng ta tạo một ra phần tử React.** Vậy làm cách nào để đảm bảo rằng đối tượng prop
không thay đổi giữa các lần hiển thị? Hy vọng rằng bây giờ bạn đã và hiểu tại sao ví dụ thứ hai ở trên không hiển thị nội dung trong Log
. Nếu chúng tạ tạo phần tử JSX một lần và sử dụng lại phần tử đó, thì chúng tôi sẽ nhận được cùng một JSX mọi lúc!
Quay trở lại ví dụ thứ hai
Bởi vì phần tử <Log />
hoàn toàn không thay đổi (Vì thế props
cũng không thay đổi), React có thể tự động cung cấp tính năng tối ưu hoá này cho chúng ta và không bận tâm đến việc hiển thị lại phần tử <Log />
vì nó cũng không cần thiết phải kết xuất lại giữa các lần.
Về cơ bản điều này giống với React.memo
ngoại trừ sẽ kiểm tra từng props
riêng lẻ. Ở đây, React đang kiểm tra toàn diện đối tượng props
của phần tử.
Vậy điều này có ý nghĩa gì đối với chúng ta?
Tóm lại, nếu bạn đang gặp sự cố về hiệu suất, hãy thử cách này:
- “Tách” các thành phần hao tốn tài nguyên ra và mang nó lên thành phần cha, nơi nó sẽ được kết xuất lại ít thường xuyên hơn.
- Sau đó, chuyển thành phần hao tốn tài nguyên này thông qua
props
Bạn có thể thấy làm như vậy, sẽ vẫn giải quyết được vấn đề về hiệu xuất mà không cần phải sử dụng React.memo
phổ biến ra khắp mọi phần tử trên ứng dụng.
Ví dụ minh hoạ
Vì lý do nó khá nặng, mình sẽ đặt đường dẫn tại đây dể bạn có thể tìm hiểu thêm