谷歌的數據表明,一個有 10 條數據 0.4 秒可以加載完的頁面,在變成 30 條數據加載時間爲 0.9 秒後,流量和廣告收入減少了 20%。當谷歌地圖的首頁文件大小從 100kb 減少到 70~80kb 時,流量在第一週漲了 10%,接下來的三週漲了 25%。
騰訊的前端工程師根據長期的數據監控也發現頁面的一秒鐘延遲會造成 9.4% 的 PV 的下降,8.3% 跳出率的增加以及 3.5% 轉化率的下降。
可以看出,性能優化商業上來說很重要。
但是,更重要的還是屏幕前我們的用戶,讓用戶在使用產品時有更快更舒適的瀏覽體驗,這算是一種前端工程師的自我修養。
所以今天就分享一下如何去優化我們的 React 項目,進而提升用戶體驗。
使用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>
);
}
}
使用 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
也已支持該特性而無需再配置。
使用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>
);
}
使用 shouldComponentUpdate() 防止不必要的重新渲染
當一個組件的 props
或 state
變更,React 會將最新返回的元素與之前渲染的元素進行對比,以此決定是否有必要更新真實的 DOM,當它們不相同時 React 會更新該 DOM。
即使 React 只更新改變了的 DOM 節點,重新渲染仍然花費了一些時間。在大部分情況下它並不是問題,但是如果渲染的組件非常多時,就會浮現性能上的問題,我們可以通過覆蓋生命週期方法 shouldComponentUpdate
來進行提速。
shouldComponentUpdate
方法會在重新渲染前被觸發。其默認實現總是返回 true,如果組件不需要更新,可以在 shouldComponentUpdate
中返回 false
來跳過整個渲染過程。其包括該組件的 render 調用以及之後的操作。
shouldComponentUpdate(nextProps, nextState) {
return nextProps.next !== this.props.next
}
使用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
對象加速嵌套數據的比較。
使用 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);
使用 ComponentDidUnmount() 刪除未使用的DOM 元素
有些時候,存在一些未使用的代碼會導致內存泄漏的問題,React 通過向我們提供componentWillUnmount
方法來解決這個問題。
componentWillUnmount()
會在組件卸載及銷燬之前直接調用。在此方法中執行必要的清理操作,例如,清除 定時器,取消網絡請求或清除在 componentDidMount()
中創建的訂閱等。
例如,我們可以在組件銷燬之前,清除一些事件處理程序:
componentWillUnmount() {
document.removeEventListener("click", this.closeMenu);
}
componentWillUnmount()
中不應調用 setState()
,因爲該組件將永遠不會重新渲染。組件實例卸載後,將永遠不會再掛載它。
其他優化技術
虛擬化長列表
如果你的應用渲染了長列表(上百甚至上千的數據),我們推薦使用“虛擬滾動”技術。這項技術會在有限的時間內僅渲染有限的內容,並奇蹟般地降低重新渲染組件消耗的時間,以及創建DOM
節點的數量。
react-window
和 react-virtualized
是熱門的虛擬滾動庫。它們提供了多種可複用的組件,用於展示列表、網格和表格數據。如果你想要一些針對你的應用做定製優化,你也可以創建你自己的虛擬滾動組件,就像 Twitter 所做的。
使用 Chrome Performance 標籤分析組件
在開發模式下,你可以通過支持的瀏覽器可視化地瞭解組件是如何 掛載、更新以及卸載的。例如:
在 Chrome 中進行如下操作:
- 臨時禁用所有的
Chrome
擴展,尤其是 React 開發者工具。他們會嚴重干擾度量結果! - 確保你是在 React 的開發模式下運行應用。
- 打開 Chrome 開發者工具的
Performance
標籤並按下Record
。 - 對你想分析的行爲進行復現。儘量在 20 秒內完成以避免 Chrome 卡住。
- 停止記錄。
- 在
User Timing
標籤下會顯示 React 歸類好的事件。
最後,我們探索了一些可以優化 React 應用程序的一些提高性能的方法,不侷限於此。我們應該根據需要有針對性的優化應用程序,因爲在某些簡單的場景中,過度的優化,可能會得不償失。