24 February 2016 on Reactreact
本文是 React 性能工程系列文章的 第二篇
(共兩篇). 在第一篇 譯文,咱們講述瞭如何使用React性能工具和一些廣泛存在的性能瓶頸,以及一些調試相關的技巧。若是你還沒閱讀上一篇文章,建議讀一讀!segmentfault
本文咱們將深刻研究調試的工做流 -- 有了這些ideas以後,咱們又要怎麼實踐呢?咱們找了一些實際開發中遇到的例子,使用 Chrome 開發工具來診斷、修復這些性能問題。(若是你有好的建議或補充,歡迎讓咱們知悉!)瀏覽器
咱們經過下面的示例代碼來看下 -- 你將看到一個用React實現的簡單版 todo list
。點擊下面 JS fiddle
中的 "RESULT" 查看交互效果、完成性能複製。咱們將一步步更新 JS fiddle
來查看性能調試。app
從這個 TodeList
開始吧。快速地輸入沒有通過優化的代碼,你會發現它運行緩慢。ide
咱們打開 Chrome 開發者工具 Timeline profiler
,它會展現瀏覽器的詳細執行狀況,包括執行用戶事件、運行JS和渲染頁面。在Input框輸入一個字符,而後停止 timeline profiler
。因爲咱們只是輸入一個簡單字符,因此這種遲緩
並不明顯,但它倒是生成性能分析所需最小信息量的最快方式。函數
咱們注意到 Event (textInput)
的長條,在腳本處理上總計耗時121.10毫秒。從 timeline profiler
能夠看出,致使性能緩慢的是腳本問題,不是樣式或計算引發的。工具
所以咱們來看下腳本處理,切換到 Profiles
面板。Timeline
展現瀏覽器的概覽而且支持JS Profile
,而Profiles
則提供多種可視化工具,容許咱們深刻研究JS-land。如下是另外一個 Profile
記錄,代表性能的緩慢不是來源於咱們的應用代碼:佈局
看下這個Profile,Total
這列根據佔用時間遞減排列,能夠看出絕大部分時間是花在React的batchedUpdates
的調用上,這點至關明確地暗示了是在React-land這一層。相反, Self
一欄評估了花費在函數自己的時間(排除耗費在子函數的時間),這樣能夠看出是否有一些特別耗時的函數。從這兩個方面看來,用戶層函數並無明顯的性能瓶頸。所以,咱們換用React的性能工具來試下。性能
爲了給這個緩慢的action生成一個測量概況,咱們在控制檯調用 React.addons.Perf.start()
, 輸入一個字符來執行這個action,隨後調用 React.addons.Perf.stop()
完成這個流程。這樣咱們就能夠看到React.addons.Perf.printWasted()
花費了一些沒必要要的時間:開發工具
第一列代表 TodoItem
是由 Todos
渲染出來的;然而,Perf.printWasted()
的打印結果代表:若是避免從新渲染,能夠節省100毫秒。這個彷佛是主要的優化項之一。
爲了診斷爲什麼 TodeItem
會浪費這麼多時間,咱們建立了一個自定義 mixin
, 並把它命名爲 WhyDidYouUpdateMixin
。把它 hook
到組件中,哪部分代碼更新及其更新的緣由都打印出來。如下就是咱們的代碼,你能夠根據本身所需,隨意適配。
一旦咱們把這個 mixin
放到 TodoItem
裏面,咱們能夠看到這樣的結果:
呀!咱們看到 tags
的 before
和 after
是同樣的 -- mixin
告訴咱們若是兩個對象相等(不嚴格相等)是能夠避免更新的。另外一方面,計算出兩個方法是否相等的過程也是很耗時的,由於 Function.bind
儘管帶一樣的參數,也會生成一個新函數。雖然這些都是有用的線索 -- 咱們回頭看下在 tags
和 deleteItem
咱們是怎麼作的,彷佛就是咱們每傳一個新的值,都建立了一個 TodoItem
。
若是咱們經過一個未綁定的函數來傳遞給TodoItem,並用一個常量來儲存tags,就能夠避免這個問題了:
如今 WhyDidYouUpdateMixin
顯示前一個props和新的props是淺相等的。咱們可使用 PureRenderMixin
,若是先後兩個props(和state)淺相等,則不用更新。
當咱們再次運行 profiler
,發現如今只是用了35毫秒(比以前快了4倍):
這樣比起以前已經好不少了,但仍不夠理想。Input 框的輸入不該該這麼耗時。所以,咱們繼續優化這個問題。剛剛僅僅是減小了常量,咱們仍然須要對每一個 item
作淺對比。
在這點上,你或許以爲一個 todo list
上面有1000個 item
已經很特殊了,30毫秒對於你的應用來講是能夠接受的。可是,若是你要支持上千個子item,這樣就不符合理想中的60fps(每幀16毫秒)。
下一步比較合理的作法是把一個組件拆分紅多個子組件 (這也能夠說是有效的第一步)。咱們注意到 Todos
組件實際上包括兩個互不相交的子組件:一個AddTaskForm
子組件包含了輸入框和按鈕,另外一個 TodoItem
子組件包含items的列表。
每一步重構都能得到性能的提高:
假設咱們用 PureRenderMixin
建立一個TodoItems組件,它不用從新渲染每一個item,就可省去部分優化工做,這時prevProps.items === this.props.items
。
假設咱們建立了一個 AddTaskForm
組件,文本輸入後的狀態就已經更新在那裏了。當輸入框文本變化時,Todos
組件就不用再從新渲染了。
這兩步結合起來,每次按鍵只須要10毫秒!
方案: 當用戶的任務項太多( >3000)時,咱們就渲染一個 warning
,而且給這些 todo items
添加樣式,這樣其它每一個item就都有一個背景顏色。
實踐:
咱們用一個相似於 todo list
的例子,伴隨着 TodoItems
的執行 -- 在這個例子中,咱們把input框中的內容儲存在組件狀態的top-level
。
咱們建立一個 TaskWarning
組件,根據任務項的數量來渲染提示信息。要在組件內部封裝這些邏輯,若是不用渲染,咱們就讓它返回null。
咱們給div:nth-child(even)
添加灰色背景。
觀察報告: 在Input框快速輸入,頁面變得有點遲緩(不超過3000個任務)。若是咱們第一次給 todo list
再添加一項( > 3000 個任務),在按下按鈕的那一瞬間,這種遲緩反而銷聲匿跡了。太使人驚訝了,添加更多的任務反而可以修復頁面遲緩的問題!
調試: timeline profile
展現了一些很是有趣的報告:
基於某種緣由,輸入一個簡單的字符會形成大量樣式被從新計算,這個會耗時30毫秒(這也是爲何當咱們輸入的速度大於 30毫秒/字符
時,能夠觀察到閃退的緣由)。
查看 First invalidated
這一行,它代表 Danger.dangerouslyReplaceNodeWithMarkup
形成佈局失效,須要從新計算樣式。如下是 react-with-addons.js: 2301
:
`oldChild.parentNode.replaceChild(newChild, oldChild);`
基於某些緣由,React用一個全新的DOM節點來替換原來的DOM節點。從新調用那些DOM操做是很耗性能的!使用 Perf.printDOM()
,能夠查看到React是怎樣進行DOM操做的:
update attributes
代表在 input
框輸入 abc
時,TaskWarning
仍是不可見的。然而,replace
指出React正準備接觸DOM來調用 TaskWarning
組件,儘管它看似有徹底一致的虛擬DOM。
正如這裏所代表的,React (<= v0.13) 使用一個 noscript
標籤來渲染 no component
, 但卻不恰當地把這兩個標籤的功能處理得不一致:末尾的 noscript
標籤是不須要用另外一個noscript
標籤來代替的。 此外,以前咱們給每一個div添加了灰色背景。基於CSS,3000個item節點裏面每一個獨立個體的渲染都取決於它的兄弟節點。每次 noscript
標籤被替換,其後的DOM節點都會從新計算它們的樣式。
爲了解決這個問題,咱們能夠這樣作:
讓 TaskWarning
返回一個空的 div
把 TaskWarning
組件移到一個 div
裏面,這樣它就不會影響到其後節點的CSS選擇器。
升級React :-)
但這是脫離本意的。這裏主要是咱們知道怎麼經過 timeline profiler
去診斷這些性能問題。
但願這章可以幫助你們瞭解 React 的性能問題是如何在開發者工具呈現出來的 -- 把 Timeline
、Profiles
和React性能工具結合起來用大有幫助。
有上千個items的 todo lists
隨意着色彷佛是彆扭的,但當渲染大量的文件和樣式表,或者構建一個電子手冊,咱們都會遇到很是類似的問題。並且,咱們仍然在壯大咱們的團隊 -- 若是你有興趣構建複雜的React apps,歡迎聯繫咱們。