React 應用效能優化的 6 條建議
原文地址: itnext.io/6-tips-for-…
譯文地址:github.com/xiao-T/note…
本文版權歸原作者所有,翻譯僅用於學習。
我第一次學習 React 時,就知道了所有的可以提高效能的小技巧。直到現在,主要的效能優化手段就是避免協調(React 通過前後的對比來決定 DOM 是否需要更新)。
這篇文章中,我將會列舉幾個簡單的方法,通過簡單的開發技巧提升 React 應用的效能。這並不意味著你應該一直使用這些技術,但是,知道這些總是有好處的。
所以,我們開始:
1. 利用渲染 bail-out 技術
父級元件每次更新,不管子元件的 props 有沒有改變,它們都會隨著更新。也就是說,即使子元件的 props 與之前的完全一致
可以繼承 React.PureComponent
(利用 shouldComponentUpdate
實現的)class 來實現元件或者用高階元件 memo
來包裝你的元件。利用這些方法,你可以保證在元件 props 改變時才會更新。
需要注意的是:如果在比較小的元件中應用這些技術(就像下面演示的一樣),將看不到有什麼好處,反而會讓你的應用變得有點慢(這是因為每次渲染
TLDR: 對於那些“複雜”的元件使用 React.PureComponent
,shouldComponentUpdate
或者 memo()
,但是,對於一些輕量的元件就沒有必要了。如果有需要,可以把一個大型元件拆分成多個小元件,以便用 memo()
來包裝。
// index.jsx
export default function ParentComponent(props) {
return (
<div>
<SomeComponent someProp={props.somePropValue}
<div>
<AnotherComponent someOtherProp={props.someOtherPropValue} />
</div>
</div>
)
}
// ./SomeComponent.jsx
export default function SomeComponent(props) {
return (
<div>{props.someProp}</div>
)
}
// --------------------------------------------
// ./AnotherComponent.jsx (1st variation)
// This component will render anytime the value of `props.somePropValue` changed
// regardless of whether the value of `props.someOtherPropValue` has changed
export default function AnotherComponent(props) {
return (
<div>{props.someOtherProp}</div>
)
}
// ./AnotherComponent.jsx (2nd variation)
// This component will render only render when its *own* props have changed
export default memo((props) => {
return (
<div>{props.someOtherProp}</div>
)
});
// ./AnotherComponent.jsx (3rd variation)
// This component will also render only render when its *own* props have changed
class AnotherComponent extends React.PureComponent {
render() {
return <div>{this.props.someOtherProp}</div>
}
}
// ./AnotherComponent.jsx (4th variation)
// Same as above,re-written
class AnotherComponent extends React.PureComponent {
shouldComponentUpdate(nextProps) {
return this.props !== nextProps
}
render() {
return <div>{this.props.someOtherProp}</div>
}
}
複製程式碼
2. 避免使用內聯物件
對於內聯物件,React 在每次渲染都會重新建立新的引用。這會導致元件每次都認為這是新的物件。因此,元件每次渲染時對比前後 props
都會返回 false
。
對於很多人來說內聯樣式就是一種間接引用。元件通過 prop 內聯 styles
將會導致元件每次都會重新渲染(除非你自定義 shouldComponentUpdate
方法),這也會有潛在的效能問題,具體取決於元件內部是否有多個子元件。
如果,不得不使用不同引用,有一個小技巧。比如,可以使用 ES6 的擴充套件運算子傳遞多個 props 的內容。只要物件的內容是原始值(不是函式、物件或者陣列)或者非原始值的“固定”引用,你都可以把它們包裝成一個 prop 傳遞,而不是作為單獨的 prop 傳遞。利用這種技巧可以讓元件在重新渲染時通過對比前後 props 帶來 bail-out 的好處。
TLDR:如果,使用內聯樣式(或者一般的物件),你將會無法從 React.PureComponent
或者 memo()
獲益。在某些情況下,你可以把需要傳遞的內容合併成一個物件,作為元件的 props 向下傳遞。
// 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} />
}
複製程式碼
3. 避免匿名函式
雖然,通過 prop 傳遞函式時匿名函式是一種非常好的方式(特別是需要其它props 作為引數呼叫時),但是,元件每次渲染都會得到不同的引用。這有點上面提到的內聯樣式。為了保證傳遞給 React 元件的方法都是同一個引用,你可以在 class 中定義方法(如果,你用的基於 class 的元件)或者使用 useCallback
保證引用的一致(如果,你是用函式元件)。某些情況下,如果,你需要為函式提供不同的引數(比如:.map
的回撥函式),你可以利用 memoize 來包裝函式(就像 lodash’s memoize)。這種行為稱為“函式快取”或者“監聽快取”,它可以利用瀏覽器記憶體動態儲存多個函式的固定引用。
當然,有些時候行內函數比較方便,而且,也不會引起效能問題。這可能是你在一些“輕量”元件上使用或者父元件每次 props 改變都需要重新渲染(因此,你不需要關心元件每次渲染時函式的引用是不是有變化)。
最後,有一件事我需要強調下:預設情況下 render-props 函式也是匿名函式。每當,把函式作為 children
元件時,你都可以在外部定義一個元件來代替這個函式,這樣會保證引用的唯一性。
TLDR:儘可能使用 useCallback
來繫結 props 方法,這樣你就可以通過 bail-out 受益。這也適用於 render-props 返回的函式。
// Don't do this!
function Component(props) {
return <AnotherComponent onChange={() => props.callback(props.id)} />
}
// Do this instead :)
function Component(props) {
const handleChange = useCallback(() => props.callback(props.id),[props.id]);
return <AnotherComponent onChange={handleChange} />
}
// Or this for class-based components :)
class Component extends React.Component {
handleChange = () => {
this.props.callback(this.props.id)
}
render() {
return <AnotherComponent onChange={this.handleChange} />
}
}
複製程式碼
4. 那些非必要的內容可以懶載入
這條看起來和本文沒多大關係,但是,減少 React 元件的大小,可以更快的顯示它們。因此,如果,你覺得某些內容沒必要初始化渲染,在初始還完成後,你可以根據需要再去載入它們。同時,也會減少應用啟動時檔案的大小,讓應用載入更快。最後,通過拆分初始化的檔案,可以把大型的工作量拆分成多個小任務,以便瀏覽器更快的響應。利用 React.Lazy
和 React.Suspense
可以輕鬆的實現檔案的拆分。
TLDR: 對於那些不是實時可見的(或者不必要),直到和使用者互動後才可見的元件,可以懶載入。
// ./Tooltip.jsx
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>
);
}
// ./Component.jsx
function Component(props) {
return (
<Tooltip title={props.title}>
<AnotherComponent />
</Tooltip>
)
}
複製程式碼
5. 調整 CSS 避免元件強制 mount & unmount
渲染的成本很高,尤其是 DOM 需要改變時。某些時候每次只需要顯示某一組內容,比如:型別手風琴或者 tab 功能,你需要臨時的 unmount 那些不可見的元件,同時,mount 可見的元件。
如果,元件的 mount/unmount 成本很高,那麼這個操作可能會導致互動的延遲。對於這種情況,比較好做法是:可以通過 CSS 先隱藏元件,但是保證 DOM 存在。我意識到有些時候這並不可能,如果,同時有多個元件 mount 會帶來一些問題(比如:多個元件之間共享同一個分頁元件時),但是,對於其它情況,你應該選擇使用剛才提到的方法。
另外,將 opacity
設定為 0 瀏覽器的成本**幾乎為 0 **(因為,並不會引起迴流),因此,儘可能的不去改變 visibility
& display
。
TLDR:相比通過 unmount 隱藏元件,有時通過 CSS 隱藏元件更加好。對於那些需要花費更多時間 mount/unmount 的大型元件更加有利。
// Avoid this is the components are too "heavy" to mount/unmount
function Component(props) {
const [view,setView] = useState('view1');
return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
}
// Do this instead if you' re opting for speed & performance gains
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>
)
}
複製程式碼
6. 快取那些成本巨大的計算
渲染總是不可避免的,但是,由於 React 元件是功能型元件,每次渲染元件內部的計算都會重新計算。使用 useMemo
hook 可以把那些不需要重新計算的值“快取”起來。這樣一來,那些成本巨大的計算可以利用上次渲染時的值。在這可以學到更多有關知識。
總的來說,就是要減少元件在渲染期間的 JavaScript 的工作量,因此,主執行緒就不會阻塞。
TLDR:利用 useMemo
快取那些成本巨大的計算
// don't do this!
function Component(props) {
const someProp = heavyCalculation(props.item);
return <AnotherComponent someProp={someProp} />
}
// do this instead. Now `someProp` will be recalculated
// only when `props.item` changes
function Component(props) {
const someProp = useMemo(() => heavyCalculation(props.item),[props.item]);
return <AnotherComponent someProp={someProp} />
}
複製程式碼
總結
我有意的沒有提到一些事情,比如:“使用生產環境構建”、“對鍵盤事件節流”或者“使用 web workers”,這是因為,我認為這些和 React 並沒有什麼關係,它們更多是與一般的 web 開發效能息息相關。這篇文章中提到的開發實踐更多的是有助於提升 React 的效能,釋放主執行緒,最終讓使用者感覺應用響應更快。
感謝閱讀:)