You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A. 什麼都沒有(App 跟 Content 的 render function 都沒被執行到)
B. 只有 render App!(只有 App 的 render function 被執行到)
C. render App! 以及 render content!(兩者的 render function 都被執行到)
When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.
大意就是你可以想像成 render function 會回傳一個 React elements 的 tree,然後 React 會把這次的 tree 跟上次的做比較,並且找出如何有效率地把這差異 update 到 UI 上面去。
前陣子正在重構公司的專案,試了一些東西之後發現自己對於 React 的渲染機制其實不太了解,不太知道 render 什麼時候會被觸發。而後來我發現不只我這樣,其實還有滿多人對這整個機制不太熟悉,因此決定寫這篇來分享自己的心得。
其實不知道怎麼優化倒還好,更慘的事情是你自以為在優化,其實卻在拖慢效能,而根本的原因就是對 React 的整個機制還不夠熟。被「優化」過的 component 反而還變慢了!這個就嚴重了。
因此,這篇文章會涵蓋到下面幾個主題:
為了判別你到底對以上這些理解多少,我們馬上進行幾個小測驗!有些有陷阱,請睜大眼睛看清楚啦!
React 小測驗
第一題
以下程式碼是個很簡單的網頁,就一個按鈕跟一個叫做
Content
的元件而已,而按鈕按下去之後會改變App
這個 component 的 state。請問:當你按下按鈕之後,console 會輸出什麼?
A. 什麼都沒有(App 跟 Content 的 render function 都沒被執行到)
B. 只有
render App!
(只有 App 的 render function 被執行到)C.
render App!
以及render content!
(兩者的 render function 都被執行到)第二題
以下程式碼也很簡單,分成三個元件:App、Table 跟 Row,由 App 傳遞 list 給 Table,Table 再用 map 把每一個 Row 都渲染出來。
而這段程式碼的問題就在於按下按鈕之後,
App
的 render function 被觸發,然後Table
的 render function 也被觸發,所以重新渲染了一次整個列表。可是呢,我們點擊按鈕之後,
list
根本沒變,其實是不需要重新渲染的,所以聰明的小明把 Table 從 Component 變成 PureComponent,只要 state 跟 props 沒變就不會重新渲染,變成下面這樣:把 Table 從 Component 換成 PureComponent 之後,如果我們再做一次同樣的操作,也就是按下
change state
按鈕改變 App 的 state,這時候會提升效率嗎?A. 會,在這情況下 PureComponent 會比 Component 有效率
B. 不會,兩者差不多
C. 不會,在這情況下 Component 會比 PureComponent 有效率
第三題
接著讓我來看一個跟上一題很像的例子,只是這次換成按按鈕以後會改變 list:
這時候 Table 的 PureComponent 優化已經沒有用了,因為 list 已經變了,所以會觸發 render function。要繼續優化的話,比較常用的手段是把 Row 變成 PureComponent,這樣就可以確保相同的 Row 不會再次渲染。
請問:把 Row 從 Component 換成 PureComponent 之後,如果我們再做一次同樣的操作,也就是按下
change state
按鈕改變 list,這時候會提升效率嗎?A. 會,在這情況下 PureComponent 會比 Component 有效率
B. 不會,兩者差不多
C. 不會,在這情況下 Component 會比 PureComponent 有效率
React 的 render 機制
在公布答案之前,先幫大家簡單複習一下 React 是如何把你的畫面渲染出來的。
首先,大家都知道你在
render
這個 function 裡面可以回傳你想渲染的東西,例如說:要注意的是這邊 return 的東西不會直接就放到 DOM 上面去,而是會先經過一層 virtual DOM。其實你可以簡單把這個 virtual DOM 想成 JavaScript 的物件,例如說上面 Content render 出來的結果可能是:
最後一步則是 React 進行 virtual DOM diff,把上次的跟這次的做比較,並且把變動的部分更新到真的 DOM 上面去。
簡單來說呢,就是在 React Component 以及 DOM 之間新增了一層 virtual DOM,先把你要渲染的東西轉成 virtual DOM,再把需要更新的東西 update 到真的 DOM 上面去。
如此一來,就能夠減少觸碰到真的 DOM 的次數並且提升性能。
舉個例子,假設我們實作一個非常簡單的,按一個按鈕之後就會改變 state 的小範例:
在程式剛開始執行時,渲染的順序是這樣的:
這時候的 virtual DOM 整體應該會長得像這樣:
當你按下按鈕,改變 state 了以後,執行順序都跟剛剛一樣:
這時候拿到的 virtual DOM 應該會長得像這樣:
而 React 的 virtual DOM diff 演算法,就會發現只有一個地方改變,然後把那邊的文字替換掉,其他部分都不會動到。
其實官方文件把這一段寫得很好:
大意就是你可以想像成 render function 會回傳一個 React elements 的 tree,然後 React 會把這次的 tree 跟上次的做比較,並且找出如何有效率地把這差異 update 到 UI 上面去。
所以說呢,如果你要成功更新畫面,你必須經過兩個步驟:
因此,要優化效能的話你有兩個方向,那就是:
我們先從後者開始吧!
提升 React 效能:保持 virtual DOM 的一致
因為有了 virtual DOM 這一層的守護,通常你不必太擔心 React 的效能。
像是我們開頭問答的第一題:
你每次按下按鈕之後,由於 App 的 state 改變了,所以會先觸發 App 的 render function,而因為裡面有回傳
<Content />
,所以也會觸發 Content 的 render function。因此你每按一次按鈕,這兩個 component 的 render function 就會個別被呼叫一次。所以答案是
C. render App! 以及 render content!(兩者的 render function 都被執行到)
可是儘管如此,真的 DOM 不會有任何變化。因為在 virtual DOM diff 的時候,React 會發現你這次跟上次的 virtual DOM 長得一模一樣(因為沒有東西改變嘛),就不會對 DOM 做任何操作。
如果能盡量維持 virtual DOM 的結構相似的話,可以減少一些不必要的操作,在這點上其實可以做的優化還很多,可以參考官方文件,裡面寫的很詳細。
提升 React 效能:不要觸發 render function
雖然不必太過擔心,但是 virtual DOM diff 也是需要執行時間的。雖然說速度很快,但再快也比不上完全不呼叫來的快,你說是吧。
對於這種「我們已經明確知道不該有變化」的情形,我們連 render 都不該呼叫,因為沒必要嘛,再怎麼呼叫都是一樣的結果。如果 render 沒有被呼叫的話,連 virtual DOM diff 都不需要執行,又提升了一些性能。
你應該有聽過
shouldComponentUpdate
這個 function,就是來做這件事的。如果你在這個 function 中回傳 false,就不會重新呼叫 render function。加上去之後,你會發現無論你按多次按鈕,Content 的 render function 都不會被觸發。
但是這個東西請小心使用,一個不注意你就會碰到 state 跟 UI 搭不上的情形,例如說 state 明明變成 world,可是 UI 顯示的還是 Hello:
在上面的例子中,按下按鈕之後 state 確實變成
world
,但是因為 Content 的shouldComponentUpdate
永遠都回傳 false,所以不會再次觸發 render,就看不到對應的新的 state 的畫面了。不過這有點極端,因為通常不會永遠都回傳 false,除非你真的確定這個 component 完全不需要 re-render。
比起這個,有一個更合理的判斷基準是:
假設
this.props
是:而
nextProps
是:那在比較的時候就會發現
props.text
變了,就可以順理成章的呼叫 render function。還有另外一點是這邊用shallowEqual
來比較前後的差異,而不是用deepEqual
。這是出於效能上的考量。別忘了,你要執行這樣的比較也是會吃資源的,尤其是在你的 object 很深很深的時候,要比較的東西可就多了,因此我們會傾向用
shallowEqual
,只要比較一層即可。另外,前面有提到
PureComponent
這個東西,其實就是 React 提供的另外一種元件,差別就是在於它自動幫你加上上面那一段的比較。如果你想看原始碼的話,在這邊:講到這邊,就可以來公佈第二題的解答了,答案是:
A. 會,在這情況下 PureComponent 會比 Component 有效率
,因為繼承了 PureComponent 之後,只要 props 跟 state 沒變,就不會執行 render function,也不會執行 virtual DOM diff,節省了許多開銷。shallowEqual 與 Immutable data structures
你剛開始在學 React 的時候,可能會被告誡說如果要更改資料,不能夠這樣寫:
而是應該要這樣:
但你知道為什麼嗎?
這個就跟我們上面講到的東西有關了。如同上面所述,其實使用
PureComponent
是一件很正常的事情,因為 state 跟 props 如果沒變的話,本來就不該觸發 render function。而剛剛也提過
PureComponent
會幫你shallowEqual
state 跟 props,決定要不要呼叫 render function。在這種情況下,如果你用了一開始講的那種寫法,就會產生問題,例如說:
在上面的程式碼中,其實
this.state.obj
跟newObject
還是指向同一個物件,指向同一塊記憶體,所以當我們在做shallowEqual
的時候,就會判斷出這兩個東西是相等的,就不會執行 render function 了。在這時候,我們就需要 Immutable data,Immutable 翻成中文就是永遠不變的,意思就是:「當一個資料被創建之後,就永遠不會變了」。那如果我需要更改資料的話怎麼辦呢?你就只能創一個新的。
有了 Immutable 的概念之後,
shallowEqual
就不會出錯了,因為如果我們有新的資料,就可以保證它是一個新的 object,這也是為什麼我們在用setState
的時候總是要產生一個新的物件,而不是直接對現有的做操作。這邊還有一個要注意的地方,那就是 spread operator 只會複製第一層的資料而已,它並不是 deep clone:
所以當你的 state 是比較複雜的結構時,要改變資料就會變得比較麻煩一點,因為你必須要對每一層都做差不多的事情,避免直接去改到你要改的物件:
若你的 state 結構很多層,那就會變得非常非常難改,這時候你有三個選擇:
註:感謝網友 KanYueh Chen 的指正,讓我補上上面這一段。
PureComponent 的陷阱
當我們遵守 Immutable 的規則之後,理所當然的就會想把所有的 Component 都設成 PureComponent,因為 PureComponent 的預設很合理嘛,資料沒變的話就不呼叫 render function,可以節省很多不必要的比較。
那讓我們回頭來看開場小測驗的最後一題:
我們把
Row
變成了 PureComponent,所以只要 state 跟 props 沒變,就不會 re-render,所以答案應該要是A. 會,在這情況下 PureComponent 會比 Component 有效率
?錯,如果你把程式碼看更清楚一點,你會發現答案其實是
C. 不會,在這情況下 Component 會比 PureComponent 有效率
。你的前提是對的,「只要 state 跟 props 沒變,就不會 re-render,PureComponent 就會比 Component 更有效率」。但其實還有另外一句話也是對的:「如果你的 state 或 props 『永遠都會變』,那 PureComponent 並不會比較快」。
所以這兩種的使用時機差異在於:state 跟 props 到底常常會變還是不會變?
上述的例子中,陷阱在於
itemStyle
這個 props,我們每次 render 的時候都創建了一個新的物件,所以對 Row 來說,儘管 props.item 是一樣的,但是 props.style 卻是「每次都不一樣」。如果你已經知道每次都會不一樣,那 PureComponent 這時候就無用武之地了,而且還更糟。為什麼?因為它幫你做了
shallowEqual
。別忘記了,
shallowEqual
也是需要執行時間的。已經知道 props 的比較每次都失敗的話,那不如不要比還會來的比較快,所以在這個情形下,Component 會比 PureComponent 有效率,因為不用做
shallowEqual
。這就是我開頭提到的需要特別注意的部分。不要以為你把每個 Component 都換成 PureComponent 就天下太平,App 變超快,效能提升好幾倍。不去注意這些細節的話,就有可能把效能越弄越糟。
最後再強調一次,如果你已經預期到某個 component 的 props 或是 state 會「很頻繁變動」,那你根本不用換成 PureComponent,因為你實作之後反而會變得更慢。
總結
在研究這些效能相關的問題時,我最推薦這篇:React, Inline Functions, and Performance,解開了很多我心中的疑惑以及帶給我很多新的想法。
例如說文末提到的 PureComponent 有時候反而會變慢,也是從這篇文章看來的,真心推薦大家抽空去看看。
前陣子跟同事一起把一個專案打掉重做,原本的共識是盡量用 PureComponent,直到我看到這篇文並且仔細思考了一下,發現如果你不知道背後的原理,還是不要輕易使用比較好。因此我就提議改成全部用 Component,等我們碰到效能問題要來優化時再慢慢調整。
最後附上一句我很喜歡的話,從React 巢狀 Component 效能優化這篇看來的(這篇也是在講最後提到的 PureComponent 的問題):
參考資料:
High Performance React: 3 New Tools to Speed Up Your Apps
reactjs - Reconciliation
reactjs- Optimizing Performance
React is Slow, React is Fast: Optimizing React Apps in Practice
Efficient React Components: A Guide to Optimizing React Performance
The text was updated successfully, but these errors were encountered: