React 應用性能優化的 6 條建議

原文地址: itnext.io/6-tips-for-…
譯文地址:github.com/xiao-T/note…
本文版權歸原做者全部,翻譯僅用於學習。html


我第一次學習 React 時,就知道了全部的能夠提升性能的小技巧。直到如今,主要的性能優化手段就是避免協調(React 經過先後的對比來決定 DOM 是否須要更新)。react

這篇文章中,我將會列舉幾個簡單的方法,經過簡單的開發技巧提高 React 應用的性能。這並不意味着你應該一直使用這些技術,可是,知道這些老是有好處的。git

因此,咱們開始:github

1. 利用渲染 bail-out 技術

父級組件每次更新,無論子組件的 props 有沒有改變,它們都會隨着更新。也就是說,即便子組件的 props 與以前的徹底一致,它們仍是會從新渲染。須要說明一下,我在這說的從新渲染,並非更新 DOM,而是會觸發 React 的協調動做,而後來決定是否真正的更新 DOM。這個過程對性能優化尤其重要,尤爲是那些大型組件樹,底層上,React 不得不運行 diff 算法來檢查組件樹先後是否有不同的地方。web

能夠繼承 React.PureComponent (利用 shouldComponentUpdate 實現的)class 來實現組件或者用高階組件 memo 來包裝你的組件。利用這些方法,你能夠保證在組件 props 改變時纔會更新。算法

須要注意的是:若是在比較小的組件中應用這些技術(就像下面演示的同樣),將看不到有什麼好處,反而會讓你的應用變得有點慢(這是由於每次渲染 React 都會作一次組件的淺對比)。所以,對於那些「複雜」的組件可使用這些技術,相反,一些比較輕量的組件就須要慎重使用。數組

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.LazyReact.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 的性能,釋放主線程,最終讓用戶感受應用響應更快。

感謝閱讀:)

相關文章
相關標籤/搜索