React性能優化(一)
在最近的工作中我們發現開發一個已經持續開發了一年的React應用在IE11瀏覽器和一個老舊的安卓設備上工作不是特別流暢,這引起了我們的注意,決定抽出一些精力對代碼做一些優化。雖然我們在日常開發中已經有意識的去寫高質量、高效率的代碼,但是由於開發人員的計算機性能都很好並且經常調試使用的chrome瀏覽器有一個強大的V8引擎所以性能問題還是經常會被忽略。
在這篇文章里,我會介紹我是如何用React Profile發現問題,並分析解決問題的。
分析工具
性能問題通常是在手動測試或調試時直觀感受到的,例如做某些操作時應用出現卡頓或是移動設備發熱的情況。但是在這種情況下不太容找到問題到底出在哪些組件中,很多時候新添加的代碼並不一定是問題的原因有可能只是個誘因。這時分析工具就非常有用了,可以幫助我們定位問題點。在這篇文章我用React+Redux的Todos例子作為演示代碼。
React DevTools
這應該是每個React開發者必備工具了,如果你還沒有,趕緊裝一個吧。
Highlight Updates
這是DevTools里提供的一個功能,在我們操作React App時可以高亮重新渲染的組件。
打開高亮功能
使用
打開之后當操作App時重新渲染的組件會有一個高亮的邊框,邊框為藍色時說明渲染時間短,黃色說明時間有點長,如果時間很長會是紅色就要注意了。這個工具會給我們一個直觀的顯示哪些組件更新了。
React Profiler
這個工具包含在React DevTools中,適用於react 16.5及之后版本。如果你的項目版本合適,會在DevTools里看到Profiler頁。
分析問題
使用工具
點Profiler右上角的圓形按鈕開始記錄分析數據,然后在todos上添加3個新的todo項,完成操作之后再點圓形按鈕結束錄制。
從App上可以看到,每當添加新的todo項時,整個todo列表和每一個todo項都重新渲染了。
然后看看Profiler中的數據。
React渲染組件主要分兩個階段:渲染(Render)和提交(commit)。渲染階段react執行組件中的render方法得到渲染結果也就是虛擬DOM,然后和上一次渲染的結果比較找出差異。提交階段,react會添加、更新或刪除真實DOM,把虛擬DOM中的內容渲染到頁面中。
右上角的1/3表示在錄制過程中一共有3次提交,圖中的每一個小條代表一次提交,黑色的是當前選中的提交,點擊不同的條可以選擇不同的提交。小條的高度和顏色反應了提交執行的時間,黃色高的條比藍色條用的時間長。
這部分是火焰圖🔥,每一個小塊對應一個組件,塊的長度、顏色和組件渲染所用時間有關,黃色長的塊說明消耗的時間更長。灰色說明這個組件在這次提交中沒有重新渲染。這里顯示的組件渲染時間是它自身渲染的時間和它所有子組件渲染時間的綜合。
找到花費時間長的組件
為了讓問題更明顯我加了30個todo到列表中,從圖中很容易發現大部分時間都花在TodoList這個組件上。這個圖里有一個容易產生困惑的地方就是Total Renders,這個數字代表的是這個組件在整個App生命周期里被渲染的次數,而不是在這次commit中被渲染的次數。
和之前只有3個todo時相比較,TodoList這個組件的渲染時間隨着todo數量的增加而增加,因為每次渲染列表時每個todo都重新渲染了。而這些渲染並不是必要,因為每個新todo都是添加到列表末尾,之前的todo位置和內容都沒有變化應該可以不再渲染。
點擊第一個todo組件,然后切換不同的commit發現todo的props一直沒有變化,這也證明了我們之前的想法是正確的。
改進方法
使用React.PureComponent或React.memo()避免不必要的組件重渲染
React.PureComponent和React.Component類似,但是React.PureComponent在shouldComponentUpdate()中對props和state做了一個淺比較,如果props和state沒有變化則不渲染組件。
React.memo()是16.8版本加入的新功能,為使用函數定義的組件提供了類似PureComponent的功能。
本例中的Todo組件使用函數方式定義的,所以把最后一行使用React.memo()重寫為 export default React.memo(Todo)
重寫后的代碼:
1 import React from 'react' 2 import PropTypes from 'prop-types' 3 4 const Todo = ({ onClick, completed, text }) => ( 5 <li 6 onClick={onClick} 7 style={{ 8 textDecoration: completed ? 'line-through' : 'none' 9 }} 10 > 11 {text} 12 </li> 13 ) 14 15 Todo.propTypes = { 16 onClick: PropTypes.func.isRequired, 17 completed: PropTypes.bool.isRequired, 18 text: PropTypes.string.isRequired 19 } 20 21 export default React.memo(Todo);
改過之后重新錄制分析數據再來看看,
嗯,並沒有什么變化,再次看todo組件的分析數據。
通過比較兩次commit,todo的props看上去沒有變化,而且todo也沒有state。不過進一步觀察,props中completed, id, text都是原始類型,我們可以確定他們是沒變的。onClick不同,這是一個函數,它是否變化同定義的方式有關。
通過查看代碼,發現onClick是一個inline的箭頭函數,問題是應該出在這里。
TodoList.js
const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> )} </ul> )
不要使用inline定義的方法或Object為props傳值
修改后的代碼如下:
TodoList.js
const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={toggleTodo} /> )} </ul> )
把原先的inline方法去掉,改為直接把toggleTodo傳給Todo。
Todo.js
const Todo = ({ onClick, id, completed, text }) => { function clickHandler() { onClick(id) } return ( <li onClick={clickHandler} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) }
在Todo.js中定義了一個clickHandler方法,這樣todo方法每次重新運行時clickHandler都不會變。
新的分析數據中可以看到前面的todo組件都是灰色說明沒有重新渲染,只有最后一個新加的todo被渲染了。
以上就是我本篇文章要分享的內容,希望對你有一點點幫助。之后我有其他發現還會繼續和大家分享。同時也希望你能提出你遇到的問題或是想法我們共同討論。
謝謝