1. 程式人生 > 前端設計 >React 應用效能優化的 6 條建議

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

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


我第一次學習 React 時,就知道了所有的可以提高效能的小技巧。直到現在,主要的效能優化手段就是避免協調(React 通過前後的對比來決定 DOM 是否需要更新)。

這篇文章中,我將會列舉幾個簡單的方法,通過簡單的開發技巧提升 React 應用的效能。這並不意味著你應該一直使用這些技術,但是,知道這些總是有好處的。

所以,我們開始:

1. 利用渲染 bail-out 技術

父級元件每次更新,不管子元件的 props 有沒有改變,它們都會隨著更新。也就是說,即使子元件的 props 與之前的完全一致

,它們還是會重新渲染。需要說明一下,我在這說的重新渲染,並不是更新 DOM,而是會觸發 React 的協調動作,然後來決定是否真正的更新 DOM。這個過程對效能優化尤為重要,尤其是那些大型元件樹,底層上,React 不得不執行 diff 演算法來檢查元件樹前後是否有不一樣的地方。

可以繼承 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 的效能,釋放主執行緒,最終讓使用者感覺應用響應更快。

感謝閱讀:)