ReactJS: Cách tránh cạm bẫy hiệu suất trong React với memo, useMemo và useCallback
Giới thiệu
Trong các ứng dụng React, các vấn đề về hiệu suất có thể đến từ độ trễ của mạng, API hoạt động quá mức, thư viện của bên thứ ba không hiệu quả và thậm chí mã có cấu trúc tốt hoạt động tốt cho đến khi nó gặp phải tải lớn bất thường. Việc xác định nguyên nhân gốc rễ của các vấn đề về hiệu suất có thể khó khăn, nhưng nhiều vấn đề trong số này bắt nguồn từ việc kết xuất component. Component hiển thị nhiều hơn mong đợi hoặc component có hoạt động nặng dữ liệu có thể khiến mỗi lần hiển thị bị chậm. Do đó, việc học cách ngăn các kết xuất không cần thiết có thể giúp tối ưu hóa hiệu suất của ứng dụng React và tạo ra trải nghiệm tốt hơn cho người dùng của bạn.
Trong hướng dẫn này, bạn sẽ tập trung vào việc tối ưu hóa hiệu suất trong các component React. Để khám phá vấn đề, bạn sẽ xây dựng một component để phân tích một khối văn bản. Bạn sẽ xem cách các hành động khác nhau có thể kích hoạt kết xuất và cách bạn có thể sử dụng Hook và memo để giảm thiểu các phép tính dữ liệu tốn kém. Đến cuối hướng dẫn này, bạn sẽ làm quen với nhiều Hook nâng cao hiệu suất, chẳng hạn như Hook useMemo
và Hook useCallback
, và các trường hợp sẽ yêu cầu chúng.
Điều kiện tiên quyết
- Bạn sẽ cần một môi trường phát triển chạy Node.js; hướng dẫn này đã được thử nghiệm trên Node.js phiên bản 10.22.0 và npm phiên bản 6.14.6. Để cài đặt phần mềm này trên macOS hoặc Ubuntu 18.04, hãy làm theo các bước trong Cách cài đặt Node.js và Tạo Môi trường Phát triển Cục bộ trên macOS hoặc phần Cài đặt Sử dụng PPA của Cách Cài đặt Node.js trên Ubuntu 18.04.
- Môi trường phát triển React được thiết lập với Create React App. Để thiết lập điều này, hãy làm theo Bước 1 - Tạo một dự án trống của hướng dẫn Cách quản lý trạng thái trên các thành phần lớp React. Hướng dẫn này sẽ sử dụng
performance-tutorial
làm tên dự án. - Nếu bạn chưa quen gỡ lỗi trong React, hãy xem hướng dẫn Cách gỡ lỗi các component trong React bằng Công cụ dành cho nhà phát triển React và tự làm quen với các công cụ dành cho nhà phát triển trong trình duyệt bạn đang sử dụng, chẳng hạn như Chrome DevTools và Firefox Developer Tools.
- Bạn cũng sẽ cần kiến thức cơ bản về JavaScript, HTML và CSS.
Bước 1 - Ngăn kết xuất lại bằng memo
Trong bước này, bạn sẽ xây dựng một component phân tích văn bản. Bạn sẽ tạo một input để lấy một khối văn bản và một component sẽ tính toán tần suất của các chữ cái và ký hiệu. Sau đó, bạn sẽ tạo ra một tình huống trong đó trình phân tích văn bản hoạt động kém và bạn sẽ xác định được nguyên nhân gốc rễ của vấn đề hiệu suất. Cuối cùng, bạn sẽ sử dụng hàm React memo
để ngăn kết xuất trên component khi component cha thay đổi, nhưng các prop cho component con không thay đổi.
Đến cuối bước này, bạn sẽ có một component hoạt động mà bạn sẽ sử dụng trong suốt phần còn lại của hướng dẫn và hiểu về cách kết xuất gốc có thể tạo ra các vấn đề về hiệu suất trong các component con.
Xây dựng một trình phân tích văn bản
Để bắt đầu, hãy thêm một phần tử <textarea>
vào App.js
.
Mở App.js
trong một trình soạn thảo văn bản mà bạn chọn, sau đó thêm <textarea>
cùng với một <label>
. Đặt nhãn bên trong một <div>
với className
là wrapper
bằng cách thêm mã được đánh dấu sau:
import './App.css';
function App() {
return(
<div className="wrapper">
<label htmlFor="text">
<p>Add Your Text Here:</p>
<textarea
id="text"
name="text"
rows="10"
cols="100"
>
</textarea>
</label>
</div>
)
}
export default App;
Thao tác này sẽ tạo một input đầu vào cho ứng dụng mẫu. Lưu file lại.
Mở App.css
để thêm một số style. Bên trong App.css
, thêm phần padding vào class .wrapper
, sau đó thêm một margin
vào các phần tử div
. Thay thế CSS bằng đoạn mã sau:
.wrapper { padding: 20px; } .wrapper div { margin: 20px 0; }
Điều này sẽ thêm sự tách biệt giữa đầu vào và hiển thị dữ liệu. Lưu file lại.
Tiếp theo, tạo một thư mục cho component CharacterMap
. Component này sẽ làm nhiệm vụ phân tích văn bản, tính toán tần suất xuất hiện của từng chữ cái và ký hiệu, đồng thời hiển thị kết quả.
Đầu tiên, tạo thư mục:
mkdir src/components/CharacterMap
Sau đó, tạo và mở CharacterMap.js
ra, tạo một component được gọi là CharacterMap
với prop là text
và hiển thị text
bên trong một <div>
:
import PropTypes from 'prop-types'; export default function CharacterMap({ text }) { return( <div> Character Map: {text} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired }
Lưu ý rằng bạn đang thêm một PropType cho phần prop text
để giới thiệu một số cách gõ yếu.
Thêm một hàm để lặp lại văn bản và trích xuất thông tin ký tự. Đặt tên cho hàm là itemize
và dùng text
làm đối số. Hàm itemize
sẽ lặp lại mọi ký tự nhiều lần và sẽ rất chậm khi kích thước văn bản tăng lên. Vậy thì đây sẽ là một cách rất tốt để kiểm tra hiệu suất:
import PropTypes from 'prop-types';
function itemize(text){
const letters = text.split('')
.filter(l => l !== ' ')
.reduce((collection, item) => {
const letter = item.toLowerCase();
return {
...collection,
[letter]: (collection[letter] || 0) + 1
}
}, {})
return letters;
}
export default function CharacterMap({ text }) {
return(
<div>
Character Map:
{text}
</div>
)
}
CharacterMap.propTypes = {
text: PropTypes.string.isRequired
}
Bên trong itemize
, bạn chuyển đổi văn bản thành một mảng bằng cách sử dụng .split trên mọi ký tự. Sau đó, bạn loại bỏ các khoảng trắng bằng phương thức .filter và sử dụng phương thức .reduce để lặp lại từng chữ cái. Bên trong phương thức .reduce
, sử dụng một đối tượng làm giá trị ban đầu, sau đó chuẩn hóa ký tự bằng cách chuyển đổi nó thành chữ thường và thêm 1
vào tổng trước đó hoặc 0
nếu không có tổng trước đó. Cập nhật đối tượng với giá trị mới trong khi vẫn giữ nguyên các giá trị trước đó bằng cách sử dụng đối tượng toán tử Spread.
Bây giờ bạn đã tạo một đối tượng với số lượng cho mỗi ký tự, bạn cần sắp xếp nó theo ký tự cao nhất. Chuyển đổi đối tượng thành một mảng các cặp với Object.entries
. Mục đầu tiên trong mảng là ký tự và mục thứ hai là số. Sử dụng phương thức .sort để đặt các ký tự phổ biến nhất lên trên:
import PropTypes from 'prop-types';
function itemize(text){
const letters = text.split('')
.filter(l => l !== ' ')
.reduce((collection, item) => {
const letter = item.toLowerCase();
return {
...collection,
[letter]: (collection[letter] || 0) + 1
}
}, {})
return Object.entries(letters)
.sort((a, b) => b[1] - a[1]);
}
export default function CharacterMap({ text }) {
return(
<div>
Character Map:
{text}
</div>
)
}
CharacterMap.propTypes = {
text: PropTypes.string.isRequired
}
Cuối cùng, gọi hàm itemize
bằng văn bản và hiển thị kết quả:
import PropTypes from 'prop-types';
function itemize(text){
const letters = text.split('')
.filter(l => l !== ' ')
.reduce((collection, item) => {
const letter = item.toLowerCase();
return {
...collection,
[letter]: (collection[letter] || 0) + 1
}
}, {})
return Object.entries(letters)
.sort((a, b) => b[1] - a[1]);
}
export default function CharacterMap({ text }) {
return(
<div>
Character Map:
{itemize(text).map(character => (
<div key={character[0]}>
{character[0]}: {character[1]}
</div>
))}
</div>
)
}
CharacterMap.propTypes = {
text: PropTypes.string.isRequired
}
Lưu file lại.
Bây giờ import component và hiển thị bên trong App.js
.
Mở App.js
ra, trước khi bạn có thể sử dụng component, bạn cần có một cách để lưu trữ văn bản. Sau đó import hàm useState và lưu trữ các giá trị trên một biến được gọi là text
và một hàm cập nhật được gọi là setText
.
Để cập nhật text
, hãy thêm một hàm vào onChange
và sẽ truyền event.target.value
đến hàm setText
:
import './App.css'; function App() { const [text, setText] = useState(''); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> </div> ) } export default App;
Lưu ý rằng bạn đang khởi tạo useState
với một chuỗi rỗng. Điều này sẽ đảm bảo rằng giá trị bạn truyền cho component CharacterMap
luôn là một chuỗi ngay cả khi người dùng chưa nhập văn bản.
Import CharacterMap
và hiển thị nó sau phần tử <label>
. Truyền state text
đến prop text
:
import { useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <CharacterMap text={text} /> </div> ) } export default App;
Lưu các file lại. Khi bạn làm như vậy, trình duyệt sẽ làm mới và khi bạn thêm văn bản, bạn sẽ tìm thấy phân tích ký tự sau khi nhập:
Như được thấy trong ví dụ, component hoạt động khá tốt với một lượng nhỏ văn bản. Với mỗi lần gõ phím, React sẽ cập nhật vào CharacterMap
với dữ liệu mới. Nhưng hiệu suất cục bộ có thể gây hiểu lầm. Không phải tất cả các thiết bị sẽ có cùng bộ nhớ với môi trường phát triển của bạn.
Kiểm tra hiệu suất
Có nhiều cách để kiểm tra hiệu suất ứng dụng của bạn. Bạn có thể thêm khối lượng lớn văn bản hoặc bạn có thể đặt trình duyệt của mình sử dụng ít bộ nhớ hơn. Để đẩy component đến mức tắc nghẽn hiệu suất, hãy sao chép Đầu vào Wikipedia cho GNU và dán nó vào hộp văn bản. Mẫu của bạn có thể hơi khác một chút tùy thuộc vào cách trang Wikipedia được chỉnh sửa.
Sau khi dán mục nhập vào hộp văn bản của bạn, hãy thử nhập ký tự bổ sung e
và lưu ý thời gian hiển thị. Sẽ có một khoảng thời gian tạm dừng đáng kể trước khi bản đồ ký tự cập nhật:
Nếu component không đủ chậm và bạn đang sử dụng Firefox, Edge hoặc một số trình duyệt khác, hãy thêm văn bản cho đến khi bạn nhận thấy nó chậm lại.
Nếu đang sử dụng Chrome, bạn có thể điều chỉnh CPU bên trong tab hiệu suất. Đây là một cách tuyệt vời để mô phỏng điện thoại thông minh hoặc một phần cứng cũ hơn. Để biết thêm thông tin, hãy xem tài liệu Chrome DevTools.
Nếu component quá chậm với mục nhập Wikipedia, hãy xóa một số văn bản. Bạn muốn nhận được sự chậm trễ đáng kể, nhưng bạn không muốn làm cho nó chậm không thể sử dụng được hoặc làm hỏng trình duyệt của bạn.
Ngăn kết xuất lại các component con
Hàm itemize
là gốc rễ của sự chậm trễ được xác định trong phần cuối cùng. Hàm thực hiện rất nhiều công việc trên mỗi mục nhập bằng cách lặp lại nhiều lần các nội dung. Có những tối ưu hóa mà bạn có thể thực hiện trực tiếp trong chính hàm, nhưng trọng tâm của hướng dẫn này là cách xử lý kết xuất component khi văn bản không thay đổi. Nói cách khác, bạn sẽ coi hàm itemize
như một hàm mà bạn không có quyền truy cập để thay đổi. Mục tiêu sẽ là chỉ chạy nó khi cần thiết. Điều này sẽ hiển thị cách xử lý hiệu suất cho các API hoặc thư viện của bên thứ ba mà bạn không thể kiểm soát.
Để bắt đầu, bạn sẽ khám phá một tình huống mà component cha thay đổi, nhưng component con không thay đổi.
Bên trong App.js
, hãy thêm một đoạn văn giải thích cách component hoạt động và một nút để chuyển đổi thông tin:
import { useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> {showExplanation && <p> This displays a list of the most common characters. </p> } <CharacterMap text={text} /> </div> ) } export default App;
Gọi Hook useReducer
có chức năng giảm tốc để đảo ngược trạng thái hiện tại. Lưu đầu ra vào showExplanation
và toggleExplanation
. Sau <label>
, hãy thêm một nút để chuyển đổi lời giải thích và một đoạn văn sẽ hiển thị khi nào showExplanation
là chuẩn.
Lưu file lại. Khi trình duyệt làm mới, hãy nhấp vào nút để chuyển đổi phần giải thích. Chú ý cách để có sự chậm trễ.
Đây là một vấn đề. Người dùng của bạn sẽ không gặp phải sự chậm trễ khi họ chuyển đổi một lượng nhỏ JSX. Sự chậm trễ xảy ra bởi vì khi component chính thay đổi - trong tình huống này là App.js
- thì component CharacterMap
đang kết xuất và tính toán lại dữ liệu ký tự. Phần prop text
giống hệt nhau, nhưng component vẫn hiển thị lại vì React sẽ hiển thị lại toàn bộ cây component khi một component cha thay đổi.
Nếu bạn lập hồ sơ ứng dụng bằng các công cụ dành cho nhà phát triển của trình duyệt, bạn sẽ phát hiện ra rằng component này hiển thị lại do component gốc thay đổi. Để xem lại cách lập hồ sơ bằng các công cụ dành cho nhà phát triển, hãy xem Cách gỡ lỗi các component của React bằng các công cụ dành cho nhà phát triển React.
Vì CharacterMap
chứa một hàm dùng nhiều tài nguyên, cho nên nó chỉ nên hiển thị lại khi prop thay đổi.
Mở CharacterMap.js
ra, import memo
, sau đó truyền component đến memo
và xuất kết quả làm mặc định:
import { memo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ ... } function CharacterMap({ text }) { return( <div> Character Map: {itemize(text).map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired } export default memo(CharacterMap);
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ tải lại và không còn sự chậm trễ sau khi bạn nhấp vào nút trước khi nhận được kết quả:
Nếu bạn nhìn vào các công cụ dành cho nhà phát triển, bạn sẽ thấy rằng component không còn hiển thị nữa:
Hàm memo
sẽ thực hiện so sánh nông các prop và sẽ chỉ hiển thị lại khi các prop thay đổi. Một phép so sánh đơn giản sẽ sử dụng tử ===
để so sánh prop trước đó với prop hiện tại.
Điều quan trọng cần nhớ là việc so sánh không miễn phí. Có một khoản chi phí hiệu suất để kiểm tra các prop, nhưng khi bạn có tác động rõ ràng về hiệu suất, chẳng hạn như một phép tính tốn nhiều tài nguyên, thì điều đó đáng để ngăn chặn kết xuất lại. Hơn nữa, vì React thực hiện một phép so sánh đơn giản, component sẽ vẫn kết xuất lại khi một phần mềm hỗ trợ là một đối tượng hoặc một hàm. Bạn sẽ đọc thêm về cách xử lý các hàm như là các prop trong Bước 3.
Ở bước này, bạn đã tạo một ứng dụng có tính toán chậm và dài. Bạn đã biết cách kết xuất gốc sẽ khiến một thành phần con kết xuất lại và cách ngăn kết xuất bằng cách sử dụng memo
. Trong bước tiếp theo, bạn sẽ ghi nhớ các hành động trong một thành phần để bạn chỉ thực hiện các hành động khi các thuộc tính cụ thể thay đổi.
Bước 2 - Lưu vào bộ nhớ đệm các tính toán dữ liệu tốn tài nguyên với useMemo
Trong bước này, bạn sẽ lưu trữ kết quả của các phép tính dữ liệu chậm bằng Hook useMemo
. Sau đó, bạn sẽ kết hợp useMemo
trong một component hiện có và đặt các điều kiện để tính toán lại dữ liệu. Đến cuối bước này, bạn sẽ có thể lưu vào bộ nhớ cache các hàm tiêu tốn tài nguyên để chúng chỉ chạy khi các phần dữ liệu cụ thể thay đổi.
Trong bước trước, phần giải thích được toggle của component là một phần của phần tử gốc. Tuy nhiên, thay vào đó, bạn có thể thêm nó vào chính component CharacterMap
. Khi bạn làm như vậy, CharacterMap
sẽ có hai thuộc tính, text
và showExplanation
và nó sẽ hiển thị lời giải thích khi nào showExplanation
là chuẩn.
Để bắt đầu, hãy mở CharacterMap.js
ra, thêm một thuộc tính mới của showExplanation
. Hiển thị văn bản giải thích khi giá trị của showExplanation
là chuẩn:
import { memo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ ... } function CharacterMap({ showExplanation, text }) { return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {itemize(text).map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default memo(CharacterMap);
Lưu file lại.
Tiếp theo, mở App.js
ra, xóa đoạn giải thích và truyền showExplanation
như một prop cho CharacterMap
:
import { useReducer, useState } from 'react';
import './App.css';
import CharacterMap from '../CharacterMap/CharacterMap';
function App() {
const [text, setText] = useState('');
const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
return(
<div className="wrapper">
<label htmlFor="text">
<p>Your Text</p>
<textarea
id="text"
name="text"
rows="10"
cols="100"
onChange={event => setText(event.target.value)}
>
</textarea>
</label>
<div>
<button onClick={toggleExplanation}>Show Explanation</button>
</div>
<CharacterMap showExplanation={showExplanation} text={text} />
</div>
)
}
export default App;
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ làm mới. Nếu bạn chuyển lời giải thích, bạn sẽ lại nhận được sự chậm trễ.
Nếu bạn nhìn vào trình mô tả, bạn sẽ phát hiện ra rằng component được hiển thị lại bởi vì phần showExplanation
hỗ trợ đã thay đổi:
Hàm memo
sẽ so sánh các prop và ngăn kết xuất lại nếu không có prop nào thay đổi, nhưng trong trường hợp này, prop showExplanation
có sự thay đổi, vì vậy toàn bộ component sẽ hiển thị lại và component sẽ chạy lại hàm itemize
.
Trong trường hợp này, bạn cần ghi nhớ các phần cụ thể của component, không phải toàn bộ component. React cung cấp một Hook đặc biệt được gọi là useMemo
mà bạn có thể sử dụng để bảo vệ các phần của component của mình khi hiển thị lại. Hook có hai đối số. Đối số đầu tiên là một hàm sẽ trả về giá trị bạn muốn ghi nhớ. Đối số thứ hai là một mảng các dependency. Nếu một dependency thay đổi, useMemo
sẽ chạy lại hàm và trả về một giá trị.
Để triển khai useMemo
, trước tiên hãy mở CharacterMap.js
ra, khai báo một biến mới được gọi là characters
. Sau đó, gọi useMemo
và truyền một hàm ẩn danh trả về giá trị itemize(text)
là đối số đầu tiên và một mảng chứa text
là đối số thứ hai. Khi useMemo
chạy, nó sẽ trả về kết quả là itemize(text)
cho biến characters
.
Thay thế lời gọi đến itemize
trong JSX bằng characters
:
import { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ ... } function CharacterMap({ showExplanation, text }) { const characters = useMemo(() => itemize(text), [text]); return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {characters.map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default memo(CharacterMap);
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ tải lại và sẽ không có độ trễ khi bạn chuyển đổi phần giải thích.
Nếu bạn định cấu hình component, bạn sẽ vẫn thấy rằng nó kết xuất lại, nhưng thời gian cần để hiển thị sẽ ngắn hơn nhiều. Trong ví dụ này, nó mất 0,7 mili giây so với 916,4 mili giây không có Hook useMemo
. Đó là bởi vì React đang kết xuất lại component, nhưng nó không chạy lại chức năng có trong Hook useMemo
. Bạn có thể bảo toàn kết quả trong khi vẫn cho phép các phần khác của component cập nhật:
Nếu bạn thay đổi văn bản trong hộp văn bản, sẽ vẫn có độ trễ vì dependency text
đã thay đổi, vì vậy useMemo
sẽ chạy lại hàm. Nếu nó không chạy lại, bạn sẽ có dữ liệu cũ. Điểm mấu chốt là nó chỉ chạy khi dữ liệu nó cần thay đổi.
Trong bước này, bạn đã ghi nhớ các phần của component của mình. Bạn đã cô lập một hàm tiêu tốn tài nguyên khỏi phần còn lại của component và chỉ sử dụng Hook useMemo
để chạy hàm khi các phụ thuộc nhất định thay đổi. Trong bước tiếp theo, bạn sẽ ghi nhớ các hàm để ngăn kết xuất so sánh đơn giản.
Bước 3 - Quản lý Kiểm tra so sánh bình đẳng với useCallback
Trong bước này, bạn sẽ xử lý các prop khó so sánh trong JavaScript. React sử dụng tính năng kiểm tra bình đẳng nghiêm ngặt khi các prop thay đổi. Việc kiểm tra này xác định thời điểm chạy lại Hook và thời điểm kết xuất lại các component. Vì rất khó so sánh các hàm và đối tượng JavaScript, nên có những trường hợp mà một prop sẽ tương tự nhau về hiệu quả, nhưng vẫn kích hoạt kết xuất lại.
Bạn có thể sử dụng Hook useCallback
để duy trì một hàm qua các lần hiển thị. Điều này sẽ ngăn các kết xuất không cần thiết khi một component cha tạo lại một hàm. Khi kết thúc bước này, bạn sẽ có thể ngăn kết xuất bằng Hook useCallback
.
Khi bạn xây dựng component CharacterMap
, bạn có thể gặp phải tình huống mà bạn cần nó linh hoạt hơn. Trong hàm itemize
, bạn luôn chuyển đổi ký tự thành chữ thường, nhưng một số người dùng thì component có thể không muốn chức năng đó. Họ có thể muốn so sánh các ký tự viết hoa và viết thường hoặc muốn chuyển đổi tất cả các ký tự thành chữ hoa.
Để tạo điều kiện thuận lợi cho việc này, hãy thêm một prop mới được gọi là transformer
sẽ thay đổi ký tự. Hàm transformer
sẽ là bất cứ thứ gì nhận một ký tự làm đối số và trả về một chuỗi của một số loại.
Bên trong CharacterMap
, thêm transformer
vào như một prop. Cung cấp cho nó một hàm PropType
với giá trị mặc định là null
:
import { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = item.toLowerCase(); return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return Object.entries(letters) .sort((a, b) => b[1] - a[1]); } function CharacterMap({ showExplanation, text, transformer }) { const characters = useMemo(() => itemize(text), [text]); return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {characters.map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired, transformer: PropTypes.func } CharacterMap.defaultProps = { transformer: null } export default memo(CharacterMap);
Tiếp theo, cập nhật itemize
để lấy transformer
làm đối số. Thay thế phương thức .toLowerCase
bằng transformer. Nếu transformer
là true, hãy gọi hàm với item
làm đối số. Nếu không thì trả về item
:
import { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text, transformer){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = transformer ? transformer(item) : item; return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return Object.entries(letters) .sort((a, b) => b[1] - a[1]); } function CharacterMap({ showExplanation, text, transformer }) { ... } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired, transformer: PropTypes.func } CharacterMap.defaultProps = { transformer: null } export default memo(CharacterMap);
Cuối cùng, cập nhật Hook useMemo
. Thêm transformer
dưới dạng dependency và truyền nó tới hàm itemize
. Bạn muốn chắc chắn rằng các dependency của bạn là đầy đủ. Điều đó có nghĩa là bạn cần thêm bất kỳ thứ gì có thể thay đổi dưới dạng dependency. Nếu người dùng thay đổi transformer
bằng cách chuyển đổi giữa các tùy chọn khác nhau, bạn cần chạy lại hàm để nhận được giá trị chính xác.
import { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text, transformer){ ... } function CharacterMap({ showExplanation, text, transformer }) { const characters = useMemo(() => itemize(text, transformer), [text, transformer]); return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {characters.map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired, transformer: PropTypes.func } CharacterMap.defaultProps = { transformer: null } export default memo(CharacterMap);
Lưu file lại.
Trong ứng dụng này, bạn không muốn cung cấp cho người dùng khả năng chuyển đổi giữa các hàm khác nhau. Nhưng bạn muốn các ký tự được viết thường. Định nghĩa một transformer
trong App.js
mà sẽ chuyển đổi ký tự thành chữ thường. Hàm này sẽ không bao giờ thay đổi, nhưng bạn cần phải truyền nó cho CharacterMap
.
Mở App.js
ra, định nghĩa một hàm được gọi là transformer
để chuyển đổi một ký tự thành chữ thường. Truyền hàm như là một prop CharacterMap
:
import { useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) const transformer = item => item.toLowerCase(); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} /> </div> ) } export default App;
Lưu file lại. Khi bạn làm như vậy, bạn sẽ thấy rằng sự chậm trễ đã quay trở lại khi bạn chuyển đổi phần giải thích.
Nếu bạn lập hồ sơ cho component, bạn sẽ thấy rằng component kết xuất lại do các prop thay đổi và các Hook đã thay đổi:
Nếu bạn quan sát kỹ hơn, bạn sẽ thấy rằng prop showExplanation
đã thay đổi, điều này có ý nghĩa vì bạn đã nhấp vào nút, nhưng prop transformer
cũng thay đổi.
Khi bạn thực hiện thay đổi state trong App
bằng cách nhấp vào nút, thì component App
được hiển thị lại và khai báo lại transformer
. Mặc dù hàm giống nhau, nhưng nó không giống với hàm trước đó về mặt tham chiếu. Điều đó có nghĩa là nó không hoàn toàn giống với hàm trước đó.
Nếu bạn mở bảng điều khiển của trình duyệt và so sánh các hàm giống hệt nhau, bạn sẽ thấy rằng so sánh là sai, như được hiển thị trong khối mã sau:
const a = () = {}; const b = () = {}; a === a // true a === b // false
Sử dụng toán tử so sánh ===
, đoạn mã này cho thấy rằng hai hàm không được coi là bằng nhau, ngay cả khi chúng có cùng giá trị.
Để tránh vấn đề này, React cung cấp một Hook được gọi là useCallback
. Hook tương tự như useMemo
: nó nhận một hàm làm đối số đầu tiên và một mảng các dependency làm đối số thứ hai. Sự khác biệt là useCallback
trả về hàm chứ không phải kết quả của hàm. Giống như Hook useMemo
, nó sẽ không tạo lại hàm trừ khi một dependency thay đổi. Điều đó có nghĩa là Hook useMemo
trong CharacterMap.js
sẽ so sánh cùng một giá trị và Hook sẽ không chạy lại.
Bên trong App.js
, import useCallback
và truyền hàm ẩn danh làm đối số đầu tiên và một mảng trống làm đối số thứ hai. Bạn không bao giờ muốn App
tạo lại hàm này:
import { useCallback, useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) const transformer = useCallback(item => item.toLowerCase(), []); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} /> </div> ) } export default App;
Lưu file lại. Khi bạn làm như vậy, bạn sẽ có thể chuyển đổi phần giải thích mà không cần chạy lại hàm.
Nếu bạn định cấu hình component, bạn sẽ thấy rằng Hook không còn chạy nữa:
Trong component cụ thể này, bạn không thực sự cần Hook useCallback
. Bạn có thể khai báo hàm bên ngoài component và nó sẽ không bao giờ hiển thị lại. Bạn chỉ nên khai báo các hàm bên trong component của mình nếu chúng yêu cầu một số loại dữ liệu prop hoặc state. Nhưng đôi khi bạn cần tạo các hàm dựa trên state bên trong hoặc prop và trong những trường hợp đó, bạn có thể sử dụng Hook useCallback
để giảm thiểu kết xuất.
Như vậy trong bước này, bạn đã bảo toàn các hàm trên các lần hiển thị bằng cách sử dụng Hook useCallback
. Bạn cũng đã học được cách các hàm đó sẽ giữ được sự bình đẳng khi được so sánh dưới dạng prop hoặc dependency trong Hook.
Phần kết luận
Bây giờ bạn có các công cụ để cải thiện hiệu suất trên các component tiêu tốn tài nguyên. Bạn có thể sử dụng memo
, useMemo
và useCallback
để tránh kết xuất component tiêu tốn tài nguyên. Nhưng tất cả các chiến lược này đều bao gồm chi phí thực hiện của riêng chúng. memo
sẽ mất thêm công việc để so sánh các thuộc tính và Hook sẽ cần phải chạy các phép so sánh bổ sung trên mỗi lần hiển thị. Chỉ sử dụng các công cụ này khi có nhu cầu rõ ràng trong dự án của bạn, nếu không, bạn có nguy cơ thêm độ trễ của chính mình.
Cuối cùng, không phải tất cả các vấn đề về hiệu suất đều yêu cầu sửa chữa kỹ thuật. Đôi khi, chi phí hiệu suất là không thể tránh khỏi — chẳng hạn như API chậm hoặc chuyển đổi dữ liệu lớn — và trong những tình huống đó, bạn có thể giải quyết vấn đề bằng cách sử dụng thiết kế bằng cách hiển thị các component tải, hiển thị trình giữ chỗ trong khi các hàm không đồng bộ đang chạy hoặc các cải tiến khác cho người dùng trải nghiệm.
Nếu bạn muốn đọc thêm các hướng dẫn về React, bạn có thể quay lại trang Cách viết mã trong chuỗi React.js.