自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React 性能優(yōu)化技巧總結(jié)

新聞 前端
本文將從 render 函數(shù)的角度總結(jié) React App 的優(yōu)化技巧。文中將涉及 React 16.8.2 版本的內(nèi)容(也即 Hooks),因此請至少了解 useState 以保證食用效果。

[[258068]]

本文將從 render 函數(shù)的角度總結(jié) React App 的優(yōu)化技巧。需要提醒的是,文中將涉及 React 16.8.2 版本的內(nèi)容(也即 Hooks),因此請至少了解 useState 以保證食用效果。

正文開始。

當(dāng)我們討論 React App 的性能問題時,組件的 渲染 速度是一個重要問題。在進(jìn)入到具體優(yōu)化建議之前,我們先要理解以下 3 點(diǎn):

  1. 當(dāng)我們在說「render」時,我們在說什么?
  2. 什么時候會執(zhí)行「render」?
  3. 在「render」過程中會發(fā)生什么?

解讀 render 函數(shù)

這部分涉及 reconciliation 和 diffing 的概念,當(dāng)然官方文檔在 這里 。

當(dāng)我們在說「render」時,我們在說什么?

這個問題其實(shí)寫過 React 的人都會知道,這里再簡單說下:

在 class 組件中,我們指的是 render 方法:

  1. class Foo extends React.Component {  
  2.  render() {  
  3.    return <h1> Foo </h1>;  
  4.  }  
  5. }  

在函數(shù)式組件中,我們指的是函數(shù)組件本身:

  1. function Foo() {  
  2.   return <h1> Foo </h1>;  
  3. }  

什么時候會執(zhí)行「render」?

render 函數(shù)會在兩種場景下被調(diào)用:

1. 狀態(tài)更新時

a. 繼承自 React.Component 的 class 組件更新狀態(tài)時

  1. import React from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. class App extends React.Component {  
  5.   render() {  
  6.     return <Foo />;  
  7.   }  
  8. }  
  9.   
  10. class Foo extends React.Component {  
  11.   state = { count: 0 };  
  12.   
  13.   increment = () => {  
  14.     const { count } = this.state;  
  15.   
  16.     const newCount = count < 10 ? count + 1 : count;  
  17.   
  18.     this.setState({ count: newCount });  
  19.   };  
  20.   
  21.   render() {  
  22.     const { count } = this.state;  
  23.     console.log("Foo render");  
  24.   
  25.     return (  
  26.       <div>  
  27.         <h1> {count} </h1>  
  28.         <button onClick={this.increment}>Increment</button>  
  29.       </div>  
  30.     );  
  31.   }  
  32. }  
  33.   
  34. const rootElement = document.getElementById("root");  
  35. ReactDOM.render(<App />, rootElement);  

可以看到,代碼中的邏輯是我們點(diǎn)擊就會更新 count,到 10 以后,就會維持在 10。增加一個 console.log,這樣我們就可以知道 render 是否被調(diào)用了。從執(zhí)行結(jié)果可以知道,即使 count 到了 10 以上,render 仍然會被調(diào)用。

總結(jié):繼承了 React.Component 的 class 組件,即使?fàn)顟B(tài)沒變化,只要調(diào)用了setState 就會觸發(fā) render。

b. 函數(shù)式組件更新狀態(tài)時

我們用函數(shù)實(shí)現(xiàn)相同的組件,當(dāng)然因?yàn)橐袪顟B(tài),我們用上了 useState hook:

  1. import React, { useState } from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. class App extends React.Component {  
  5.   render() {  
  6.     return <Foo />;  
  7.   }  
  8. }  
  9.   
  10. function Foo() {  
  11.   const [count, setCount] = useState(0);  
  12.   
  13.   function increment() {  
  14.     const newCount = count < 10 ? count + 1 : count;  
  15.     setCount(newCount);  
  16.   }  
  17.   
  18.   console.log("Foo render");  
  19.     
  20.   return (  
  21.     <div>  
  22.       <h1> {count} </h1>  
  23.       <button onClick={increment}>Increment</button>  
  24.     </div>  
  25.   );  
  26. }  
  27.   
  28. const rootElement = document.getElementById("root");  
  29. ReactDOM.render(<App />, rootElement);  

我們可以注意到,當(dāng)狀態(tài)值不再改變之后,render 的調(diào)用就停止了。

總結(jié):對函數(shù)式組件來說,狀態(tài)值改變時才會觸發(fā) render 函數(shù)的調(diào)用。

2. 父容器重新渲染時

  1. import React from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. class App extends React.Component {  
  5.   state = { name: "App" };  
  6.   render() {  
  7.     return (  
  8.       <div className="App">  
  9.         <Foo />  
  10.         <button onClick={() => this.setState({ name: "App" })}>  
  11.           Change name  
  12.         </button>  
  13.       </div>  
  14.     );  
  15.   }  
  16. }  
  17.   
  18. function Foo() {  
  19.   console.log("Foo render");  
  20.   
  21.   return (  
  22.     <div>  
  23.       <h1> Foo </h1>  
  24.     </div>  
  25.   );  
  26. }  
  27.   
  28. const rootElement = document.getElementById("root");  
  29. ReactDOM.render(<App />, rootElement);  

只要點(diǎn)擊了 App 組件內(nèi)的 Change name 按鈕,就會重新 render。而且可以注意到,不管 Foo 具體實(shí)現(xiàn)是什么,F(xiàn)oo 都會被重新渲染。

總結(jié):無論組件是繼承自 React.Component 的 class 組件還是函數(shù)式組件,一旦父容器重新 render,組件的 render 都會再次被調(diào)用。

在「render」過程中會發(fā)生什么?

只要 render 函數(shù)被調(diào)用,就會有兩個步驟按順序執(zhí)行。這兩個步驟非常重要,理解了它們才好知道如何去優(yōu)化 React App。

Diffing

在此步驟中,React 將新調(diào)用的 render 函數(shù)返回的樹與舊版本的樹進(jìn)行比較,這一步是 React 決定如何更新 DOM 的必要步驟。雖然 React 使用高度優(yōu)化的算法執(zhí)行此步驟,但仍然有一定的性能開銷。

Reconciliation

基于 diffing 的結(jié)果,React 更新 DOM 樹。這一步因?yàn)樾枰遁d和掛載 DOM 節(jié)點(diǎn)同樣存在許多性能開銷。

開始我們的 Tips

Tip #1:謹(jǐn)慎分配 state 以避免不必要的 render 調(diào)用

我們以下面為例,其中 App 會渲染兩個組件:

  1. CounterLabel  
  2. List  
  1. import React, { useState } from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. const ITEMS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];  
  5.   
  6. function App() {  
  7.   const [count, setCount] = useState(0);  
  8.   const [items, setItems] = useState(ITEMS);  
  9.   return (  
  10.     <div className="App">  
  11.       <CounterLabel count={count} increment={() => setCount(count + 1)} />  
  12.       <List items={items} />  
  13.     </div>  
  14.   );  
  15. }  
  16.   
  17. function CounterLabel({ count, increment }) {  
  18.   return (  
  19.     <>  
  20.       <h1>{count} </h1>  
  21.       <button onClick={increment}> Increment </button>  
  22.     </>  
  23.   );  
  24. }  
  25.   
  26. function List({ items }) {  
  27.   console.log("List render");  
  28.   
  29.   return (  
  30.     <ul>  
  31.       {items.map((item, index) => (  
  32.         <li key={index}>{item} </li>  
  33.       ))}  
  34.     </ul>  
  35.   );  
  36. }  
  37.   
  38. const rootElement = document.getElementById("root");  
  39. ReactDOM.render(<App />, rootElement);  

執(zhí)行上面代碼可知,只要父組件 App 中的狀態(tài)被更新, CounterLabel 和 List 就都會更新。

當(dāng)然, CounterLabel 重新渲染是正常的,因?yàn)?count 發(fā)生了變化,自然要重新渲染;但是對于 List 而言,就完全是不必要的更新了,因?yàn)樗匿秩九c count 無關(guān)。 盡管 React 并不會在 reconciliation 階段真的更新 DOM,畢竟完全沒變化,但是仍然會執(zhí)行 diffing 階段來對前后的樹進(jìn)行對比,這仍然存在性能開銷。

還記得 render 執(zhí)行過程中的 diffing 和 reconciliation 階段嗎?前面講過的東西在這里碰到了。

因此,為了避免不必要的 diffing 開銷,我們應(yīng)當(dāng)考慮將特定的狀態(tài)值放到更低的層級或組件中(與 React 中所說的「提升」概念剛好相反)。在這個例子中,我們可以通過將 count 放到 CounterLabel 組件中管理來解決這個問題。

Tip #2:合并狀態(tài)更新

因?yàn)槊看螤顟B(tài)更新都會觸發(fā)新的 render 調(diào)用,那么更少的狀態(tài)更新也就可以更少的調(diào)用 render 了。

我們知道,React class 組件有 componentDidUpdate(prevProps, prevState) 的鉤子,可以用來檢測 props 或 state 有沒有發(fā)生變化。盡管有時有必要在 props 發(fā)生變化時再觸發(fā) state 更新,但我們總可以避免在一次 state 變化后再進(jìn)行一次 state 更新這種操作:

  1. import React from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. function getRange(limit) {  
  5.   let range = [];  
  6.   
  7.   for (let i = 0; i < limit; i++) {  
  8.     range.push(i);  
  9.   }  
  10.   
  11.   return range;  
  12. }  
  13.   
  14. class App extends React.Component {  
  15.   state = {  
  16.     numbers: getRange(7),  
  17.     limit: 7  
  18.   };  
  19.   
  20.   handleLimitChange = e => {  
  21.     const limit = e.target.value;  
  22.     const limitChanged = limit !== this.state.limit;  
  23.   
  24.     if (limitChanged) {  
  25.       this.setState({ limit });  
  26.     }  
  27.   };  
  28.   
  29.   componentDidUpdate(prevProps, prevState) {  
  30.     const limitChanged = prevState.limit !== this.state.limit;  
  31.     if (limitChanged) {  
  32.       this.setState({ numbers: getRange(this.state.limit) });  
  33.     }  
  34.   }  
  35.   
  36.   render() {  
  37.     return (  
  38.       <div>  
  39.         <input  
  40.           onChange={this.handleLimitChange}  
  41.           placeholder="limit"  
  42.           value={this.state.limit}  
  43.         />  
  44.         {this.state.numbers.map((number, idx) => (  
  45.           <p key={idx}>{number} </p>  
  46.         ))}  
  47.       </div>  
  48.     );  
  49.   }  
  50. }  
  51.   
  52. const rootElement = document.getElementById("root");  
  53. ReactDOM.render(<App />, rootElement);  

這里渲染了一個范圍數(shù)字序列,即范圍為 0 到 limit。只要用戶改變了 limit 值,我們就會在 componentDidUpdate 中進(jìn)行檢測,并設(shè)定新的數(shù)字列表。

毫無疑問,上面的代碼是可以滿足需求的,但是,我們?nèi)匀豢梢赃M(jìn)行優(yōu)化。

上面的代碼中,每次 limit 發(fā)生改變,我們都會觸發(fā)兩次狀態(tài)更新:***次是為了修改 limit,第二次是為了修改展示的數(shù)字列表。這樣一來,每次 limit 的變化會帶來兩次 render 開銷:

  1. // 初始狀態(tài)  
  2. { limit: 7, numbers: [0, 1, 2, 3, 4, 5, 6]  
  3. // 更新 limit -> 4  
  4. render 1: { limit: 4, numbers: [0, 1, 2, 3, 4, 5, 6] } //   
  5. render 2: { limit: 4, numbers: [0, 2, 3]  

我們的代碼邏輯帶來了下面的問題:

  • 我們觸發(fā)了比實(shí)際需要更多的狀態(tài)更新;
  • 我們出現(xiàn)了「不連續(xù)」的渲染結(jié)果,即數(shù)字列表與 limit 不匹配。

為了改進(jìn),我們應(yīng)避免在不同的狀態(tài)更新中改變數(shù)字列表。事實(shí)上,我們可以在一次狀態(tài)更新中搞定:

  1. import React from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. function getRange(limit) {  
  5.   let range = [];  
  6.   
  7.   for (let i = 0; i < limit; i++) {  
  8.     range.push(i);  
  9.   }  
  10.   
  11.   return range;  
  12. }  
  13.   
  14. class App extends React.Component {  
  15.   state = {  
  16.     numbers: [1, 2, 3, 4, 5, 6],  
  17.     limit: 7  
  18.   };  
  19.   
  20.   handleLimitChange = e => {  
  21.     const limit = e.target.value;  
  22.     const limitChanged = limit !== this.state.limit;  
  23.     if (limitChanged) {  
  24.       this.setState({ limit, numbers: getRange(limit) });  
  25.     }  
  26.   };  
  27.   
  28.   render() {  
  29.     return (  
  30.       <div>  
  31.         <input  
  32.           onChange={this.handleLimitChange}  
  33.           placeholder="limit"  
  34.           value={this.state.limit}  
  35.         />  
  36.         {this.state.numbers.map((number, idx) => (  
  37.           <p key={idx}>{number} </p>  
  38.         ))}  
  39.       </div>  
  40.     );  
  41.   }  
  42. }  
  43.   
  44. const rootElement = document.getElementById("root");  
  45. ReactDOM.render(<App />, rootElement);  

Tip #3:使用 PureComponent 和 React.memo 以避免不必要的 render 調(diào)用

我們在之前的例子中看到將特定狀態(tài)值放到更低的層級來避免不必要渲染的方法,不過這并不總是有用。

我們來看下下面的例子:

  1. import React, { useState } from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. function App() {  
  5.   const [isFooVisible, setFooVisibility] = useState(false);  
  6.   
  7.   return (  
  8.     <div className="App">  
  9.       {isFooVisible ? (  
  10.         <Foo hideFoo={() => setFooVisibility(false)} />  
  11.       ) : (  
  12.         <button onClick={() => setFooVisibility(true)}>Show Foo </button>  
  13.       )}  
  14.       <Bar name="Bar" />  
  15.     </div>  
  16.   );  
  17. }  
  18.   
  19. function Foo({ hideFoo }) {  
  20.   return (  
  21.     <>  
  22.       <h1>Foo</h1>  
  23.       <button onClick={hideFoo}>Hide Foo</button>  
  24.     </>  
  25.   );  
  26. }  
  27.   
  28. function Bar({ name }) {  
  29.   return <h1>{name}</h1>;  
  30. }  
  31.   
  32. const rootElement = document.getElementById("root");  
  33. ReactDOM.render(<App />, rootElement);  

可以看到,只要父組件 App 的狀態(tài)值 isFooVisible 發(fā)生變化,F(xiàn)oo 和 Bar 就都會被重新渲染。

這里因?yàn)闉榱藳Q定 Foo 是否要被渲染出來,我們需要將 isFooVisible 放在 App中維護(hù),因此也就不能將狀態(tài)拆出放到更低的層級。不過,在 isFooVisible 發(fā)生變化時重新渲染 Bar 仍然是不必要的,因?yàn)?Bar 并不依賴 isFooVisible。我們只希望 Bar 在傳入屬性 name 變化時重新渲染。

那我們該怎么搞呢?兩種方法。

其一,對 Bar 做記憶化(memoize):

  1. const Bar = React.memo(function Bar({name}) {  
  2.   return <h1>{name}</h1>;  
  3. });  

這就能保證 Bar 只在 name 發(fā)生變化時才重新渲染。

此外,另一個方法就是讓 Bar 繼承 React.PureComponent 而非 React.Component:

  1. class Bar extends React.PureComponent {  
  2.  render() {  
  3.    return <h1>{name}</h1>;  
  4.  }  
  5. }  

是不是很熟悉?我們經(jīng)常提到使用 React.PureComponent 能帶來一定的性能提升,避免不必要的 render。

總結(jié):避免組件不必要的渲染的方法有:React.memo 包裹的函數(shù)式組件,繼承自 React.PureComponent 的 class 組件。

為什么不讓每個組件都繼承 PureComponent 或者用 memo 包呢?

如果這條建議可以讓我們避免不必要的重新渲染,那我們?yōu)槭裁床话衙總€ class 組件變成 PureComponent、把每個函數(shù)式組件用 React.memo 包起來?為什么有了更好的方法還要保留 React.Component 呢?為什么函數(shù)式組件不默認(rèn)記憶化呢?

毫無疑問,這些方法并不總是萬靈藥。

嵌套對象的問題

我們先來考慮下 PureComponent 和 React.memo 的組件到底做了什么?

每次更新的時候(包括狀態(tài)更新或上層組件重新渲染),它們就會在新 props、state 和舊 props、state 之間對 key 和 value 進(jìn)行淺比較。淺比較是個嚴(yán)格相等的檢查,如果檢測到差異,render 就會執(zhí)行:

  1. // 基本類型的比較  
  2. shallowCompare({ name: 'bar'}, { name: 'bar'}); // output: true  
  3. shallowCompare({ name: 'bar'}, { name: 'bar1'}); // output: false  

盡管基本類型(如字符串、數(shù)字、布爾)的比較可以工作的很好,但對象這類復(fù)雜的情況可能就會帶來意想不到的行為:

  1. shallowCompare({ name: {first: 'John', last: 'Schilling'}},  
  2.                { name: {first: 'John', last: 'Schilling'}}); // output: false  

上述兩個 name 對應(yīng)的對象的引用是不同的。

我們重新看下之前的例子,然后修改我們傳入 Bar 的 props:

  1. import React, { useState } from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. const Bar = React.memo(function Bar({ name: { first, last } }) {  
  5.   console.log("Bar render");  
  6.   
  7.   return (  
  8.     <h1>  
  9.       {first} {last}  
  10.     </h1>  
  11.   );  
  12. });  
  13.   
  14. function Foo({ hideFoo }) {  
  15.   return (  
  16.     <>  
  17.       <h1>Foo</h1>  
  18.       <button onClick={hideFoo}>Hide Foo</button>  
  19.     </>  
  20.   );  
  21. }  
  22.   
  23. function App() {  
  24.   const [isFooVisible, setFooVisibility] = useState(false);  
  25.   
  26.   return (  
  27.     <div className="App">  
  28.       {isFooVisible ? (  
  29.         <Foo hideFoo={() => setFooVisibility(false)} />  
  30.       ) : (  
  31.         <button onClick={() => setFooVisibility(true)}>Show Foo</button>  
  32.       )}  
  33.       <Bar name={{ first: "John", last: "Schilling" }} />  
  34.     </div>  
  35.   );  
  36. }  
  37.   
  38. const rootElement = document.getElementById("root");  
  39. ReactDOM.render(<App />, rootElement);  

盡管 Bar 做了記憶化且 props 值并沒有發(fā)生變動,每次父組件重新渲染時它仍然會重新渲染。這是因?yàn)楸M管每次比較的兩個對象擁有相同的值,引用并不同。

函數(shù) props 的問題

我們也可以把函數(shù)作為 props 向組件傳遞,當(dāng)然,在 JavaScript 中函數(shù)也會傳遞引用,因此淺比較也是基于其傳遞的引用。

因此,如果我們傳遞的是箭頭函數(shù)(匿名函數(shù)),組件仍然會在父組件重新渲染時重新渲染。

Tip #4:更好的 props 寫法

前面的問題的一種解決方法是改寫我們的 props。

我們不傳遞對象作為 props,而是 將對象拆分成基本類型 :

  1. <Bar firstName="John" lastName="Schilling" />  

而對于傳遞箭頭函數(shù)的場景,我們可以代以只***聲明過一次的函數(shù),從而總可以拿到相同的引用,如下所示:

  1. class App extends React.Component{  
  2.   constructor(props) {  
  3.     this.doSomethingMethod = this.doSomethingMethod.bind(this);      
  4.   }  
  5.   doSomethingMethod () { // do something}  
  6.     
  7.   render() {  
  8.     return <Bar onSomething={this.doSomethingMethod} />  
  9.   }  
  10. }  

Tip #5:控制更新

還是那句話,任何方法總有其適用范圍。

第三條建議雖然處理了不必要的更新問題,但我們也不總能使用它。

而第四條,在某些情況下我們并不能拆分對象,如果我們傳遞了某種嵌套確實(shí)復(fù)雜的數(shù)據(jù)結(jié)構(gòu),那我們也很難將其拆分開來。

不僅如此,我們也不總能傳遞只聲明了一次的函數(shù)。比如在我們的例子中,如果 App 是個函數(shù)式組件,恐怕就不能做到這一點(diǎn)了(在 class 組件中,我們可以用 bind 或者類內(nèi)箭頭函數(shù)來保證 this 的指向及***聲明,而在函數(shù)式組件中則可能會有些問題)。

幸運(yùn)的是, 無論是 class 組件還是函數(shù)式組件,我們都有辦法控制淺比較的邏輯 。

在 class 組件中,我們可以使用生命周期鉤子 shouldComponentUpdate(prevProps, prevState)來返回一個布爾值,當(dāng)返回值為 true 時才會觸發(fā) render。

而如果我們使用 React.memo,我們可以傳遞一個比較函數(shù)作為第二個參數(shù)。

注意! React.memo 的第二參數(shù)(比較函數(shù))和 shouldComponentUpdate 的邏輯是相反的,只有當(dāng)返回值為 false 的時候才會觸發(fā) render。 參考文檔 。

  1. const Bar = React.memo(  
  2.   function Bar({ name: { first, last } }) {  
  3.     console.log("update");  
  4.     return (  
  5.       <h1>  
  6.         {first} {last}  
  7.       </h1>  
  8.     );  
  9.   },  
  10.   (prevProps, newProps) =>  
  11.     prevProps.name.first === newProps.name.first &&  
  12.     prevProps.name.last === newProps.name.last  
  13. );  

盡管這條建議是可行的,但我們?nèi)砸⒁?nbsp;比較函數(shù)的性能開銷 。如果 props 對象過深,反而會消耗不少的性能。

總結(jié)

上述場景仍不夠全面,但多少能帶來一些啟發(fā)性思考。當(dāng)然在性能方面,我們還有許多其他的問題需要考慮,但遵守上述的準(zhǔn)則仍能帶來相當(dāng)不錯的性能提升。

責(zé)任編輯:張燕妮 來源: segmentfault.com
相關(guān)推薦

2021-08-27 14:26:06

開發(fā)技能React

2016-12-19 10:00:00

React性能優(yōu)化

2021-06-17 08:59:45

React前端優(yōu)化

2018-11-20 10:50:00

Java性能優(yōu)化編程技巧

2010-04-21 12:49:57

Oracle性能

2009-06-16 16:39:49

Hibernate性能

2011-07-11 15:26:49

性能優(yōu)化算法

2019-08-21 10:53:29

.NET性能優(yōu)化

2013-06-08 14:19:05

性能優(yōu)化KVM

2022-08-03 09:11:31

React性能優(yōu)化

2023-11-01 17:57:56

React應(yīng)用程序性能

2009-12-09 17:33:22

PHP性能優(yōu)化

2012-07-23 10:22:15

Python性能優(yōu)化優(yōu)化技巧

2022-05-23 13:44:53

前端開發(fā)優(yōu)化

2012-09-11 15:43:32

HBase

2011-06-14 14:17:23

性能優(yōu)化系統(tǒng)層次

2015-09-15 08:30:23

Android代碼優(yōu)化

2011-06-14 11:14:10

性能優(yōu)化代碼

2024-10-09 23:32:50

2011-06-14 14:32:46

性能優(yōu)化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號