使用React.Memo來緩存組件
提升應用程序性能的一種方法是實現memoization。Memoization是一種優化技術,主要通過存儲昂貴的函數調用的結果,並在再次發生相同的輸入時返回緩存的結果,以此來加速程序。
父組件的每次狀態更新,都會導致子組件重新渲染,即使傳入子組件的狀態沒有變化,為了減少重復渲染,我們可以使用React.memo來緩存組件,這樣只有當傳入組件的狀態值發生變化時才會重新渲染。如果傳入相同的值,則返回緩存的組件。示例如下:
export default React.memo((props) => {
return (
<div>{props.value}</div>
)
});
使用useMemo緩存大量的計算
有時渲染是不可避免的,但如果您的組件是一個功能組件,重新渲染會導致每次都調用大型計算函數,這是非常消耗性能的,我們可以使用新的useMemo鈎子來“記憶”這個計算函數的計算結果。這樣只有傳入的參數發生變化后,該計算函數才會重新調用計算新的結果。
通過這種方式,您可以使用從先前渲染計算的結果來挽救昂貴的計算耗時。總體目標是減少JavaScript在呈現組件期間必須執行的工作量,以便主線程被阻塞的時間更短。
// 避免這樣做
function Component(props) {
const someProp = heavyCalculation(props.item);
return <AnotherComponent someProp={someProp} />
}
// 只有 `props.item` 改變時someProp的值才會被重新計算
function Component(props) {
const someProp = useMemo(() => heavyCalculation(props.item), [props.item]);
return <AnotherComponent someProp={someProp} />
}
使用React.PureComponent , shouldComponentUpdate
父組件狀態的每次更新,都會導致子組件的重新渲染,即使是傳入相同props。但是這里的重新渲染不是說會更新DOM,而是每次都會調用diif算法來判斷是否需要更新DOM。這對於大型組件例如組件樹來說是非常消耗性能的。
在這里我們就可以使用React.PureComponent , shouldComponentUpdate生命周期來確保只有當組件props狀態改變時才會重新渲染。如下例子:
export default function ParentComponent(props) {
return (
<div>
<SomeComponent someProp={props.somePropValue}
<div>
<AnotherComponent someOtherProp={props.someOtherPropValue} />
</div>
</div>
)
}
export default function SomeComponent(props) {
return (
<div>{props.someProp}</div>
)
}
// 只要props.somePropValue 發生變化,不論props.someOtherPropValue是否發生變化該組件都會發生變化
export default function AnotherComponent(props) {
return (
<div>{props.someOtherProp}</div>
)
}
我們可以使用React.PureComponent 或shouldComponentUpdate 進行如下優化:
// 第一種優化
class AnotherComponent extends React.PureComponent {
render() {
return <div>{this.props.someOtherProp}</div>
}
}
//第二種優化
class AnotherComponent extends Component {
shouldComponentUpdate(nextProps) {
return this.props !== nextProps
}
render() {
return <div>{this.props.someOtherProp}</div>
}
}
PureComponent會進行淺比較來判斷組件是否應該重新渲染,對於傳入的基本類型props,只要值相同,淺比較就會認為相同,對於傳入的引用類型props,淺比較只會認為傳入的props是不是同一個引用,如果不是,哪怕這兩個對象中的內容完全一樣,也會被認為是不同的props。
需要注意的是在對於那些可以忽略渲染時間的組件或者是狀態一直變化的組件則要謹慎使用PureComponent,因為進行淺比較也會花費時間,這種優化更適用於大型的展示組件上。大型組件也可以拆分成多個小組件,並使用memo來包裹小組件,也可以提升性能。
避免使用內聯對象
使用內聯對象時,react會在每次渲染時重新創建對此對象的引用,這會導致接收此對象的組件將其視為不同的對象,因此,該組件對於prop的淺層比較始終返回false,導致組件一直重新渲染。
許多人使用的內聯樣式的間接引用,就會使組件重新渲染,可能會導致性能問題。為了解決這個問題,我們可以保證該對象只初始化一次,指向相同引用。另外一種情況是傳遞一個對象,同樣會在渲染時創建不同的引用,也有可能導致性能問題,我們可以利用ES6擴展運算符將傳遞的對象解構。這樣組件接收到的便是基本類型的props,組件通過淺層比較發現接受的prop沒有變化,則不會重新渲染。示例如下:
// Don't do this!
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
}
// Do this instead :)
const styles = { margin: 0 };
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={styles} {...aProp} />
}
避免使用匿名函數
雖然匿名函數是傳遞函數的好方法(特別是需要用另一個prop作為參數調用的函數),但它們在每次渲染上都有不同的引用。這類似於上面描述的內聯對象。為了保持對作為prop傳遞給React組件的函數的相同引用,您可以將其聲明為類方法(如果您使用的是基於類的組件)或使用useCallback鈎子來幫助您保持相同的引用(如果您使用功能組件)。前端培訓
當然,有時內聯匿名函數是最簡單的方法,實際上並不會導致應用程序出現性能問題。這可能是因為在一個非常“輕量級”的組件上使用它,或者因為父組件實際上必須在每次props更改時重新渲染其所有內容。因此不用關心該函數是否是不同的引用,因為無論如何,組件都會重新渲染。
// 避免這樣做
function Component(props) {
return <AnotherComponent onChange={() => props.callback(props.id)} />
}
// 優化方法一
function Component(props) {
const handleChange = useCallback(() => props.callback(props.id), [props.id]);
return <AnotherComponent onChange={handleChange} />
}
// 優化方法二
class Component extends React.Component {
handleChange = () => {
this.props.callback(this.props.id)
}
render() {
return <AnotherComponent onChange={this.handleChange} />
}
}
延遲加載不是立即需要的組件
延遲加載實際上不可見(或不是立即需要)的組件,React加載的組件越少,加載組件的速度就越快。因此,如果您的初始渲染感覺相當粗糙,則可以在初始安裝完成后通過在需要時加載組件來減少加載的組件數量。同時,這將允許用戶更快地加載您的平台/應用程序。最后,通過拆分初始渲染,您將JS工作負載拆分為較小的任務,這將為您的頁面提供響應的時間。這可以使用新的React.Lazy和React.Suspense輕松完成。
// 延遲加載不是立即需要的組件
const MUITooltip = React.lazy(() => import('@material-ui/core/Tooltip'));
function Tooltip({ children, title }) {
return (
<React.Suspense fallback={children}>
<MUITooltip title={title}>
{children}
</MUITooltip>
</React.Suspense>
);
}
function Component(props) {
return (
<Tooltip title={props.title}>
<AnotherComponent />
</Tooltip>
)
}
調整CSS而不是強制組件加載和卸載
渲染成本很高,尤其是在需要更改DOM時。每當你有某種手風琴或標簽功能,例如想要一次只能看到一個項目時,你可能想要卸載不可見的組件,並在它變得可見時將其重新加載。如果加載/卸載的組件“很重”,則此操作可能非常消耗性能並可能導致延遲。在這些情況下,最好通過CSS隱藏它,同時將內容保存到DOM。
盡管這種方法並不是萬能的,因為安裝這些組件可能會導致問題(即組件與窗口上的無限分頁競爭),但我們應該選擇在不是這種情況下使用調整CSS的方法。另外一點,將不透明度調整為0對瀏覽器的成本消耗幾乎為0(因為它不會導致重排),並且應盡可能優先於更該visibility 和 display。
有時在保持組件加載的同時通過CSS隱藏可能是有益的,而不是通過卸載來隱藏。對於具有顯著的加載/卸載時序的重型組件而言,這是有效的性能優化手段。
// 避免對大型的組件頻繁對加載和卸載
function Component(props) {
const [view, setView] = useState('view1');
return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
}
// 使用該方式提升性能和速度
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
function Component(props) {
const [view, setView] = useState('view1');
return (
<React.Fragment>
<SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
<AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
</React.Fragment>
)
}
使用React.Fragment避免添加額外的DOM
有些情況下,我們需要在組件中返回多個元素,例如下面的元素,但是在react規定組件中必須有一個父元素。
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
復制代碼
因此你可能會這樣做,但是這樣做的話即使一切正常,也會創建額外的不必要的div。這會導致整個應用程序內創建許多無用的元素:
function Component() {
return (
<div>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</div>
)
}
復制代碼
實際上頁面上的元素越多,加載所需的時間就越多。為了減少不必要的加載時間,我們可以使React.Fragment來避免創建不必要的元素。
function Component() {
return (
<React.Fragment>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</React.Fragment>
)
}
復制代碼
總結
我們文中列出的基本上是React內部提供的性能優化方法,這些方法可以幫助React更好地執行,並沒有列出例如Immutable.js第三方工具庫的優化方法。其實性能優化的方法有很多,但正如上面所說的,合適的方法也要在合適的場景下使用,過度的使用性能優化反而會得不償失。