招聘:《宇宙第三前端團隊招人啦!》javascript
招聘:《宇宙第三前端團隊招人啦!》html
招聘:《宇宙第三前端團隊招人啦!》前端
性能優化不是一個簡單的事情,但在 95% 以上的 React 項目中,是不須要考慮的,按本身的想法奔放的使用就能夠了。java
我認爲性能優化最好的時候是項目啓動時。在項目啓動時,須要充分考慮頁面的複雜度,若是很是複雜,則必須提早制定各類措施,防止出現性能問題。若是前期評估頁面不復雜,那大機率不會出現什麼性能問題。最慘的事情就是前期沒有評估,中後期碰到了性能問題,解決起來就至關棘手了。react
這篇文章會分享 React 項目常見的性能分析手段及優化手段,碰到性能問題的同窗能夠看看,沒碰到性能問題的同窗也須要提早預警了。ios
說到性能分析,固然要有一些指標,來度量如今網頁「卡」的程度,並指導咱們持續改進。chrome 自帶的 Performance,通常就足夠咱們進行分析了。git
我寫了一個簡單的卡頓的例子,咱們嘗試經過 Performance 來分析出這個例子中哪一行代碼卡。首先你能夠打開這個示例頁面,在這個頁面的 input 框中輸入的時候,你能明顯感受到很是卡頓。github
從上面的動圖能夠看到,最後上面一欄出現不少紅線,這就表明性能出問題了。web
咱們看下 Frames(幀) 這一欄,能看到紅框中在一次輸入中,776.9 ms 內都是 1 fps 的。這表明什麼意思?咱們知道正常網頁刷新頻率通常是 60 幀,也就是 16.67ms(1s/60)必需要刷新一次,不然就會有卡頓感,刷新時間越長,就越卡頓,在當前例子中,咱們輸入字符後,776.9 ms 後才觸發更新,能夠說是至關至關卡了。chrome
咱們知道 JS 是單線程的,也就是執行代碼與繪製是同一個線程,必須等代碼執行完,才能開始繪製。那具體是那一塊代碼執行時間長了呢?這裏咱們就要看 Main 這一欄,這一欄列出了 JS 調用棧。
在 Main 這一欄中,能夠看到咱們的 KeyPress 事件執行了 771.03ms,而後往上託動,就能看到 KeyPress 中 JS 的執行棧,能找到每一個函數的執行時間。
拖動到最下面,你能夠看到 onChange 函數執行了很長時間,點擊它,你能夠在下面看到這個函數的具體信息,點擊 demo1.js:7
甚至能看到每一行執行了多長時間。
罪魁禍首找到了,第九行代碼執行了 630ms,找到問題所在,就好解決了。
這是一個最簡單的例子,這種由單個地方引發的性能問題,也是比較好解決的。找到它、修改它、解決它!
React.Profiler 是 React 提供的,分析組件渲染次數、開始時間及耗時的一個 API,你能夠在官網找到它的文檔。
固然咱們不須要每一個組件都去加一個 React.Profiler 包裹,在開發環境下,React 會默記錄每一個組件的信息,咱們能夠經過 Chrome Profiler Tab 總體分析。
固然咱們的 Chrome 須要安裝 React 擴展,才能在工具欄中找到 Profiler
的 Tab。
Profiler 的用法和 Performance 用法差很少,點擊開始記錄,操做頁面,而後中止記錄,就會產出相關數據。
我找了一張比較複雜的圖來作個示例,圖中的數字分別表示:本次操做 React 作了 26 次 commit,第 14 次 commit 耗時最長,該次 commit 從 3.4s 時開始,消耗了 89.1 ms。
同時咱們切換到 Ranked 模式,能夠看到該次 commit,每一個組件的耗時排名。好比下圖表示 MarkdownText
組件耗時最長,達到 13.7 ms。
經過 React.Profiler,咱們能夠清晰的看到 React 組件的執行次數及時間,爲咱們優化性能指明瞭方向。
但咱們須要注意的是,React.Profiler 記錄的是 commit 階段的數據。React 的執行分爲兩個階段:
componentDidMount
和 componentDidUpdate
之類的生命週期函數。因此 React.Profiler 的分析範圍是有限的,好比咱們最開始的 input 示例,經過 React Profiler 是分析不出來性能問題的。
若是全部的性能問題都像上面這麼簡單就行了。某個點耗時極長,找到它並改進之,皆大歡喜。但在 React 項目中,最容易出現的問題是組件太多,每一個組件執行 1ms,一百個組件就執行了 100ms,怎麼優化?沒有任何一個突出的點能夠攻克,咱們也不可能把一百個組件都優化成 0.01 ms。
class App extend React.Component{ constructor(props){ super(props); this.state={ count: 0 } } render(){ return ( <div> <A /> <B /> <C /> <D /> <Button onClick={()=>{ this.setState({count: 1}) }}>click</Button> </div> ) } } 複製代碼
就像上面這個組件同樣,當咱們點擊 Button 更新 state 時,A/B/C/D 四個組件均會執行一次 render 計算,這些計算徹底是無用的。當咱們組件夠多時,會逐漸成爲性能瓶頸!咱們目標是減小沒必要要的 render。
說到避免 Render,固然第一時間想到的就是 ShouldComponentUpdate 這個生命週期,該生命週期經過判斷 props 及 state 是否變化來手動控制是否須要執行 render。固然若是使用 PureComponent,組件會自動處理 ShouldComponentUpdate。
使用 PureComponent/ShouldComponentUpdate 時,須要注意幾點:
React.memo 與 PureComponent 同樣,但它是爲函數組件服務的。React.memo 會對 props 進行淺比較,若是一致,則不會再執行了。
const App = React.memo(()=>{ return <div></div> }); 複製代碼
固然,若是你的數據不是 immutable 的,你能夠經過 React.memo 的第二個參數來手動進行深比較,一樣極其不推薦。
React.memo 對 props 的變化作了優化,避免了無用的 render。那 state 要怎麼控制呢?
const [state, setState] = useState(0); 複製代碼
React 函數組件的 useState,其 setState 會自動作淺比較,也就是若是你在上面例子中調用了 setState(0)
,函數組件會忽略此次更新,並不會執行 render 的。通常在使用的時候要注意這一點,常常有同窗掉進這個坑裏面。
React.useMemo 是 React 內置 Hooks 之一,主要爲了解決函數組件在頻繁 render 時,無差異頻繁觸發無用的昂貴計算 ,通常會做爲性能優化的手段之一。
const App = (props)=>{ const [boolean, setBoolean] = useState(false); const [start, setStart] = useState(0); // 這是一個很是耗時的計算 const result = computeExpensiveFunc(start); } 複製代碼
在上面例子中, computeExpensiveFunc
是一個很是耗時的計算,可是當咱們觸發 setBoolean
時,組件會從新渲染, computeExpensiveFunc
會執行一次。此次執行是毫無心義的,由於 computeExpensiveFunc
的結果只與 start
有關係。
React.useMemo 就是爲了解決這個問題誕生的,它能夠指定只有當 start
變化時,才容許從新計算新的 result
。
const result = useMemo(()=>computeExpensiveFunc(start), [start]); 複製代碼
我建議 React.useMemo 要多用,能用就用,避免性能浪費。
在函數組件中,React.useCallback 也是性能優化的手段之一。
const OtherComponent = React.memo(()=>{ ... }); const App = (props)=>{ const [boolan, setBoolean] = useState(false); const [value, setValue] = useState(0); const onChange = (v)=>{ axios.post(`/api?v=${v}&state=${state}`) } return ( <div> {/* OtherComponent 是一個很是昂貴的組件 */} <OtherComponent onChange={onChange}/> </div> ) } 複製代碼
在上面的例子中, OtherComponent
是一個很是昂貴的組件,咱們要避免無用的 render。雖然 OtherComponent
已經用 React.memo 包裹起來了,但在父組件每次觸發 setBoolean
時, OtherComponent
仍會頻繁 render。
由於父級組件 onChange
函數在每一次 render 時,都是新生成的,致使子組件淺比較失效。經過 React.useCallback,咱們可讓 onChange 只有在 state 變化時,才從新生成。
const onChange = React.useCallback((v)=>{ axios.post(`/api?v=${v}&state=${state}`) }, [state]) 複製代碼
經過 useCallback 包裹後, boolean
的變化不會觸發 OtherComponent
,只有 state
變化時,纔會觸發,能夠避免不少無用的 OtherComponent
執行。
可是仔細想一想, state
變化其實也是沒有必要觸發 OtherComponent
的,咱們只要保證 onChange
必定能訪問到最新的 state
,就能夠避免 state
變化時,觸發 OtherComponent
的 render。
const onChange = usePersistFn((v)=>{ axios.post(`/api?v=${v}&state=${state}`) }) 複製代碼
上面的例子,咱們使用了 Umi Hooks 的 usePersistFn,它能夠保證函數地址永遠不會變化,不管什麼時候, onChange
地址都不會變化,也就是不管什麼時候, OtherComponent
都不會從新 render 了。
Context 是跨組件傳值的一種方案,但咱們須要知道,咱們沒法阻止 Context 觸發的 render。
不像 props 和 state,React 提供了 API 進行淺比較,避免無用的 render,Context 徹底沒有任何方案能夠避免無用的渲染。
有幾點關於 Context 的建議:
Redux 中的一些細節,稍不注意,就會觸發無用的 render,或者其它的坑。
const App = (props)=>{ return ( <div> {props.project.id} </div> ) } export default connect((state)=>{ layout: state.layout, project: state.project, user: state.user })(App); 複製代碼
在上面的例子中,App 組件顯示聲明依賴了 redux 的 layout
、 project
、 user
數據,在這三個數據變化時,都會觸發 App 從新 render。
可是 App 只須要監聽 project.id
的變化,因此精細化依賴能夠避免無效的 render,是一種有效的優化手段。
const App = (props)=>{ return ( <div> {props.projectId} </div> ) } export default connect((state)=>{ projectId: state.project.id, })(App); 複製代碼
咱們常常會不當心直接操做 redux 源數據,致使意料以外的 BUG。
咱們知道,JS 中的 數組/對象 是地址引用的。在下面的例子中,咱們直接操做數組,並不會改變數據的地址。
const list = ['1']; const oldList = list; list.push('a'); list === oldList; //true 複製代碼
在 Redux 中,就常常犯這樣的錯誤。下面的例子,當觸發 PUSH
後,直接修改了 state.list
,致使 state.list
的地址並無變化。
let initState = { list: ['1'] } function counterReducer(state, action) { switch (action.type) { case 'PUSH': state.list.push('2'); return { list: state.list } default: return state; } } 複製代碼
若是組件中使用了 ShouldComponentUpdate
或者 React.memo
,淺比較 props.list === nextProps.list
,會阻止組件更新,致使意料以外的 BUG。
因此若是大量使用了 ShouldComponentUpdate
與 React.meo
,則必定要保證依賴數據的不可變性!建議使用 immer.js 來操做複雜數據。
在項目初期,必定要考慮項目的複雜度,及早採起有效的措施,防止產生性能問題。若是在中後期才考慮性能問題,則難度會增長數十倍不止。