「框架篇」React 中 的 9 種優化技術
- 2019 年 10 月 4 日
- 筆記
Google的數據表明,一個有 10 條數據 0.4 秒可以載入完的頁面,在變成 30 條數據載入時間為 0.9 秒後,流量和廣告收入減少了 20%。當Google地圖的首頁文件大小從 100kb 減少到 70~80kb 時,流量在第一周漲了 10%,接下來的三周漲了 25%。
騰訊的前端工程師根據長期的數據監控也發現頁面的一秒鐘延遲會造成 9.4% 的 PV 的下降,8.3% 跳出率的增加以及 3.5% 轉化率的下降。
可以看出,性能優化商業上來說很重要。
但是,更重要的還是螢幕前我們的用戶,讓用戶在使用產品時有更快更舒適的瀏覽體驗,這算是一種前端工程師的自我修養。
所以今天就分享一下如何去優化我們的 React 項目,進而提升用戶體驗。
1
使用React.Fragment 來避免向 DOM 添加額外的節點
我們在寫 React 程式碼時,會經常遇到返回一組元素的情況,程式碼像這樣:
class Parent extends React.Component { render() { return ( <h1>Hello there!</h1> <h1>Hello there again!</h1> ) } }
如果我們寫成這樣,控制台會報錯誤:JSX parent expressions must have one parent element ,告訴我們只能返回一個元素,所以我們通常會在最外層包裹一個 div 元素,如下所示:
class Parent extends React.Component { render() { return ( <div> <h1>Hello there!</h1> <h1>Hello there again!</h1> </div> ) } }
這樣做雖然能正常執行,但是會額外創建不必要的 DOM 節點,這可能會導致創建許多無用的元素,並且在我們的渲染數據來自特定順序的子組件時,某些情況下也會生成許多無效的節點。請考慮以下程式碼:
class Table extends React.Component { render() { return ( <table> <tr> <Columns /> </tr> </table> ); } } class Columns extends React.Component { render() { return ( <div> <td>column one</td> <td>column two</td> </div> ); } }
上面的程式碼將在我們的組件中呈現以下內容:
<table> <tr> <div> <td>column one</td> <td>column two</td> </div> </tr> </table>
這顯然不是我們想看到的,React 為我們提供了 Fragments,Fragments 允許我們將子列表分組,而無需向 DOM 添加額外節點。我們可以將組件重新編寫為:
class Columns extends React.Component { render() { return ( <React.Fragment> <td>column one</td> <td>column two</td> </React.Fragment> ); } }
2
使用 React.Lazy 延遲載入組件
有時我們只想在請求時載入部分組件,例如,僅在單擊購物車圖標時載入購物車數據,在用戶滾動到該點時在長影像列表的底部載入影像等。
React.Lazy 幫助我們按需載入組件,從而減少我們應用程式的載入時間,因為只載入我們所需的組件。
React.lazy 接受一個函數,這個函數需要動態調用 import()。它必須返回一個 Promise,該 Promise 需要 resolve 一個 defalut export 的 React 組件。如下所示:
class MyComponent extends Component{ render() { return (<div>MyComponent</div>) } } const MyComponent = React.lazy(()=>import('./MyComponent.js')) function App() { return (<div><MyComponent /></div>) }
在編譯時,使用 Webpack 解析到該語法時,它會自動地開始進行程式碼分割。最終,我們的應用程式將會被分成含有多個 UI 片段的包,這些 UI 片段將在需要時載入,如果你使用 Create React App,該功能已配置好,你能立刻使用這個特性。Next.js 也已支援該特性而無需再配置。
3
使用React.Suspense
在交換組件時,會出現一個小的時間延遲,例如在 MyComponent 組件渲染完成後,包含 OtherComponent 的模組還沒有被載入完成,這可能就會出現白屏的情況,我們可以使用載入指示器為此組件做優雅降級,這裡我們使用 Suspense 組件來解決。
React.Suspense 用於包裝延遲組件以在載入組件時顯示後備內容。
// MyComponent.js const Mycomponent = React.lazy(()=>import('./component.js')) function App() { return ( <div> <Suspense fallback={<div>loading ..</div>}> <MyComponent /> </Suspense> </div>) }
上面的程式碼中,fallback 屬性接受任何在組件載入過程中你想展示的 React 元素。
你可以將 Suspense 組件置於懶載入組件之上的任何位置,你甚至可以用一個 Suspense 組件包裹多個懶載入組件。
const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <section> <OtherComponent /> <AnotherComponent /> </section> </Suspense> </div> ); }
5
使用 shouldComponentUpdate() 防止不必要的重新渲染
當一個組件的 props 或 state 變更,React 會將最新返回的元素與之前渲染的元素進行對比,以此決定是否有必要更新真實的 DOM,當它們不相同時 React 會更新該 DOM。
即使 React 只更新改變了的 DOM 節點,重新渲染仍然花費了一些時間。在大部分情況下它並不是問題,但是如果渲染的組件非常多時,就會浮現性能上的問題,我們可以通過覆蓋生命周期方法 shouldComponentUpdate 來進行提速。
shouldComponentUpdate 方法會在重新渲染前被觸發。其默認實現總是返回 true,如果組件不需要更新,可以在 shouldComponentUpdate 中返回 false 來跳過整個渲染過程。其包括該組件的 render 調用以及之後的操作。
shouldComponentUpdate(nextProps, nextState) { return nextProps.next !== this.props.next }
6
使用React.PureComponent
React.PureComponent 與 React.Component 很相似。兩者的區別在於 React.Component並未實現 shouldComponentUpdate(),而 React.PureComponent 中以淺層對比 prop 和 state 的方式來實現了該函數。
如果賦予 React 組件相同的 props 和 state,render() 函數會渲染相同的內容,那麼在某些情況下使用 React.PureComponent 可提高性能。
// 使用 React.PureComponent class MyComponent extends React.PureComponent { render() { return (<div>MyComponent</div>) } } class MyComponent extends React.Component { render() { return (<div>MyComponent</div>) } }
React.PureComponent 中的 shouldComponentUpdate() 僅作對象的淺層比較。如果對象中包含複雜的數據結構,則有可能因為無法檢查深層的差別,產生錯誤的比對結果。僅在你的 props 和 state 較為簡單時,才使用 React.PureComponent,或者在深層數據結構發生變化時調用 forceUpdate() 來確保組件被正確地更新。你也可以考慮使用 immutable 對象加速嵌套數據的比較。
7
使用 React.memo 來快取組件
React.memo 使用了快取,快取技術用於通過存儲昂貴的函數調用的結果來加速程式,並在再次發生相同的輸入時返回快取的結果。
如果你的函數組件在給定相同 props 的情況下渲染相同的結果,那麼你可以通過將其包裝在 React.memo 中調用,以此通過記憶組件渲染結果的方式來提高組件的性能表現。這意味著在這種情況下,React 將跳過渲染組件的操作並直接復用最近一次渲染的結果。
默認情況下其只會對複雜對象做淺層對比,如果你想要控制對比過程,那麼請將自定義的比較函數通過第二個參數傳入來實現。
const MyComponent = ({user}) =>{ const {name, occupation} = user; return ( <div> <h4>{name}</h4> <p>{occupation}</p> </div> ) } // 比較函數 function areEqual(prevProps, nextProps) { /* 如果把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 否則返回 false */ } export default React.memo(MyComponent, areEqual);
8
使用 ComponentDidUnmount() 刪除未使用的DOM 元素
有些時候,存在一些未使用的程式碼會導致記憶體泄漏的問題,React 通過向我們提供componentWillUnmount 方法來解決這個問題。
componentWillUnmount() 會在組件卸載及銷毀之前直接調用。在此方法中執行必要的清理操作,例如,清除 定時器,取消網路請求或清除在 componentDidMount() 中創建的訂閱等。
例如,我們可以在組件銷毀之前,清除一些事件處理程式:
componentWillUnmount() { document.removeEventListener("click", this.closeMenu); }
componentWillUnmount() 中不應調用 setState(),因為該組件將永遠不會重新渲染。組件實例卸載後,將永遠不會再掛載它。
9
其他優化技術
虛擬化長列表
如果你的應用渲染了長列表(上百甚至上千的數據),我們推薦使用「虛擬滾動」技術。這項技術會在有限的時間內僅渲染有限的內容,並奇蹟般地降低重新渲染組件消耗的時間,以及創建 DOM 節點的數量。
react-window 和 react-virtualized 是熱門的虛擬滾動庫。它們提供了多種可復用的組件,用於展示列表、網格和表格數據。如果你想要一些針對你的應用做訂製優化,你也可以創建你自己的虛擬滾動組件,就像 Twitter 所做的。
使用 Chrome Performance 標籤分析組件
在開發模式下,你可以通過支援的瀏覽器可視化地了解組件是如何 掛載、更新以及卸載的。例如:

在 Chrome 中進行如下操作:
- 臨時禁用所有的 Chrome 擴展,尤其是 React 開發者工具。他們會嚴重干擾度量結果!
- 確保你是在 React 的開發模式下運行應用。
- 打開 Chrome 開發者工具的 Performance 標籤並按下 Record。
- 對你想分析的行為進行復現。盡量在 20 秒內完成以避免 Chrome 卡住。
- 停止記錄。
- 在 User Timing 標籤下會顯示 React 歸類好的事件。
最後,我們探索了一些可以優化 React 應用程式的一些提高性能的方法,不局限於此。我們應該根據需要有針對性的優化應用程式,因為在某些簡單的場景中,過度的優化,可能會得不償失。
文章轉載自公眾號 前端infoQ , 作者 Leiy