【譯】React性能工程(下) -- 深刻研究React性能調試

24 February 2016 on Reactreact

本文是 React 性能工程系列文章的 第二篇(共兩篇). 在第一篇 譯文,咱們講述瞭如何使用React性能工具和一些廣泛存在的性能瓶頸,以及一些調試相關的技巧。若是你還沒閱讀上一篇文章,建議讀一讀!segmentfault

本文咱們將深刻研究調試的工做流 -- 有了這些ideas以後,咱們又要怎麼實踐呢?咱們找了一些實際開發中遇到的例子,使用 Chrome 開發工具來診斷、修復這些性能問題。(若是你有好的建議或補充,歡迎讓咱們知悉!)瀏覽器

咱們經過下面的示例代碼來看下 -- 你將看到一個用React實現的簡單版 todo list。點擊下面 JS fiddle 中的 "RESULT" 查看交互效果、完成性能複製。咱們將一步步更新 JS fiddle 來查看性能調試。app

實例研究 #1: TodoList

從這個 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 裏面,咱們能夠看到這樣的結果:

呀!咱們看到 tagsbeforeafter 是同樣的 -- mixin 告訴咱們若是兩個對象相等(不嚴格相等)是能夠避免更新的。另外一方面,計算出兩個方法是否相等的過程也是很耗時的,由於 Function.bind 儘管帶一樣的參數,也會生成一個新函數。雖然這些都是有用的線索 -- 咱們回頭看下在 tagsdeleteItem 咱們是怎麼作的,彷佛就是咱們每傳一個新的值,都建立了一個 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毫秒!

實例研究 #2:

方案: 當用戶的任務項太多( >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 的性能問題是如何在開發者工具呈現出來的 -- 把 TimelineProfiles 和React性能工具結合起來用大有幫助。

有上千個items的 todo lists 隨意着色彷佛是彆扭的,但當渲染大量的文件和樣式表,或者構建一個電子手冊,咱們都會遇到很是類似的問題。並且,咱們仍然在壯大咱們的團隊 -- 若是你有興趣構建複雜的React apps,歡迎聯繫咱們

相關文章
相關標籤/搜索