摘要:應(yīng)用主要的的性能瓶頸來自于一些冗余的程序處理以及組件中的的過程。為了避免這種情況,在你的應(yīng)用中盡可能多的讓返回。使用工具將幫助你找到應(yīng)用程序中特定的性能問題。這個工具跟用起來很像,但是它是專門用來檢測應(yīng)用性能的。
這段時間對自己寫的React應(yīng)用的性能做了一些分析以及優(yōu)化,發(fā)現(xiàn)項(xiàng)目中產(chǎn)生性能問題的原因主要來自兩個方面:
大量的數(shù)據(jù)渲染使組件進(jìn)行不必要的diff過程,導(dǎo)致應(yīng)用卡頓;
部分交互操作頻繁的組件中使用了一些不必要的DOM操作,以及在處理比如scroll事件,resize事件等這類容易導(dǎo)致瀏覽器不停重新渲染的操作時,混雜了大量的計(jì)算以及混亂的DOM操作,導(dǎo)致瀏覽器卡頓。
今天主要想討論的是關(guān)于第一點(diǎn)的優(yōu)化,至于第二點(diǎn),這并不是React為我們帶來的問題,而是我們對瀏覽器的渲染機(jī)制理解和思考還有所欠缺的表現(xiàn),下次我們再去深入的探討這個問題。
這篇文章是我在探查原因的時候,在Medium上看到的。原文地址:Performance optimisations for React applications
先聲明,作者的觀點(diǎn)并不是完全可取的,他在文章中的闡述是基于React與Redux的,但事實(shí)上他并沒有完全使用Redux的connect()函數(shù),這一點(diǎn)Dan也在Tweet上指出了。不過即使這樣,對我們單純的使用React來說,也是很有意義的。針對Dan的觀點(diǎn),作者也在文章的評論中進(jìn)行了補(bǔ)充。
TL;DR;
React應(yīng)用主要的的性能瓶頸來自于一些冗余的程序處理以及組件中的DOM diff的過程。為了避免這種情況,在你的應(yīng)用中盡可能多的讓shouldComponentUpdate返回false。
并且你還要加快這個過程:
shouldComponentUpdate中的條件判斷應(yīng)該盡可能的快
shouldComponentUpdate中的條件判斷要盡可能的簡單
以下是正文
是什么造成了React應(yīng)用的性能瓶頸?不需要更新DOM的冗余處理
對大量不需要更新的DOM節(jié)點(diǎn)進(jìn)行diff計(jì)算(雖然Diff算法是使React應(yīng)用表現(xiàn)良好的關(guān)鍵,但這些計(jì)算并不能夠完全忽略不計(jì))
React中默認(rèn)的渲染方式是什么?讓我來研究下React是如何渲染一個組件的
首次render對于首次渲染來說,所有的節(jié)點(diǎn)都應(yīng)當(dāng)被渲染(綠色的表示被渲染的節(jié)點(diǎn))
圖中的每個節(jié)點(diǎn)都被渲染了,我們的應(yīng)用目前處于初始狀態(tài)。
我們想要更新一段數(shù)據(jù),而跟這個數(shù)據(jù)相關(guān)的只有一個節(jié)點(diǎn)。
我們只想渲染到達(dá)葉子節(jié)點(diǎn)的關(guān)鍵路徑。
如果我們什么都不做的話,React默認(rèn)會這樣做:(orange = waste)
所有的節(jié)點(diǎn)都被渲染了。
每個React組件中都有一個shouldComponentUpdate(nextProps, nextState)方法。它返回一個Bool值,當(dāng)組件應(yīng)當(dāng)被渲染時返回true,不應(yīng)當(dāng)被渲染時返回false。當(dāng)return false時,組件中的render方法壓根不會被執(zhí)行。然而在React中,即便你沒有明確的定義shouldComponentUpdate方法,shouldComponentUpdate還是會默認(rèn)返回True。
// default behaviour shouldComponentUpdate(nextProps, nextState) { return true; }
這意味著,當(dāng)我們對默認(rèn)行為不做任何修改時,每次改動頂層的props,整個應(yīng)用的所有組件都會執(zhí)行其render方法。這就是產(chǎn)生性能問題的原因。
那么我們?nèi)绾螌?shí)現(xiàn)理想化的更新操作呢?在你的應(yīng)用中盡可能多的讓shouldComponentUpdate返回false。
并且你還要加快這個過程:
shouldComponentUpdate中的條件判斷應(yīng)該盡可能的快
shouldComponentUpdate中的條件判斷要盡可能的簡單
加快shouldComponentUpdate中的條件判斷理想化的情況中,我們不應(yīng)該在shouldComponentUpdate函數(shù)中進(jìn)行深度比較,因?yàn)樯疃缺容^是比較消耗資源的一件事,特別是我們的數(shù)據(jù)結(jié)構(gòu)嵌套特別深,數(shù)據(jù)量特別大的時候。
class Item extends React.Component { shouldComponentUpdate(nextProps) { // expensive! return isDeepEqual(this.props, nextProps); } // ... }
一個可供替代的方法是在一個數(shù)據(jù)發(fā)生改變時,修改對象的引用而不是它的值。
const newValue = { ...oldValue // any modifications you want to do }; // fast check - only need to check references newValue === oldValue; // false // you can also use the Object.assign syntax if you prefer const newValue2 = Object.assign({}, oldValue); newValue2 === oldValue; // false
可以把這個技巧用在Redux中的reducer中:
// in this Redux reducer we are going to change the description of an item export default (state, action) => { if(action.type === "ITEM_DESCRIPTION_UPDATE") { const {itemId, description} = action; const items = state.items.map(item => { // action is not relevant to this item - we can return the old item unmodified if(item.id !== itemId) { return item; } // we want to change this item // this will keep the "value" of the old item but // return a new object with an updated description return { ...item, description }; }); return { ...state, items }; } return state; }
如果你采用了這種方式,那么在你的shouldComponentUpdate方法中只需要檢查對象的引用就可以。
// super fast - all you are doing is checking references! shouldComponentUpdate(nextProps) { return !isObjectEqual(this.props, nextProps); }
下面是isObjectEqual函數(shù)的一種簡易實(shí)現(xiàn):
const isObjectEqual = (obj1, obj2) => { if(!isObject(obj1) || !isObject(obj2)) { return false; } // are the references the same? if (obj1 === obj2) { return true; } // does it contain objects with the same keys? const item1Keys = Object.keys(obj1).sort(); const item2Keys = Object.keys(obj2).sort(); if (!isArrayEqual(item1Keys, item2Keys)) { return false; } // does every object in props have the same reference? return item2Keys.every(key => { const value = obj1[key]; const nextValue = obj2[key]; if (value === nextValue) { return true; } // special case for arrays - check one level deep return Array.isArray(value) && Array.isArray(nextValue) && isArrayEqual(value, nextValue); }); }; const isArrayEqual = (array1 = [], array2 = []) => { if (array1 === array2) { return true; } // check one level deep return array1.length === array2.length && array1.every((item, index) => item === array2[index]); };讓shouldComponentUpdate中的條件判斷更簡單
下面是一個比較復(fù)雜的shouldComponentUpdate函數(shù):
// Data structure with good separation of concerns (normalised data) const state = { items: [ { id: 5, description: "some really cool item" } ], // an object to represent the users interaction with the system interaction: { selectedId: 5 } };
這樣的數(shù)據(jù)結(jié)構(gòu)讓你的shouldComponentUpdate函數(shù)變得復(fù)雜:
import React, {Component, PropTypes} from "react"; class List extends Component { propTypes = { items: PropTypes.array.isRequired, interaction: PropTypes.object.isRequired } shouldComponentUpdate (nextProps) { // have any of the items changed? if(!isArrayEqual(this.props.items, nextProps.items)){ return true; } // everything from here is horrible. // if interaction has not changed at all then when can return false (yay!) if(isObjectEqual(this.props.interaction, nextProps.interaction)){ return false; } // at this point we know: // 1. the items have not changed // 2. the interaction has changed // we need to find out if the interaction change was relevant for us const wasItemSelected = this.props.items.any(item => { return item.id === this.props.interaction.selectedId }); const isItemSelected = nextProps.items.any(item => { return item.id === nextProps.interaction.selectedId }); // return true when something has changed // return false when nothing has changed return wasItemSelected !== isItemSelected; } render() {問題1:龐大的shouldComponentUpdate函數(shù){this.props.items.map(item => { const isSelected = this.props.interaction.selectedId === item.id; return (} }- ); })}
從上面的例子就可以看出來,即便是那么一小段很簡單的數(shù)據(jù)結(jié)構(gòu),shouldConponentUpdate函數(shù)依然有如此繁雜的處理。這是因?yàn)檫@個函數(shù)需要了解數(shù)據(jù)結(jié)構(gòu),以及每個數(shù)據(jù)之間又怎樣的關(guān)聯(lián)。所以說,shouldComponentUpdate函數(shù)的大小和復(fù)雜度,是由數(shù)據(jù)結(jié)構(gòu)決定的。
這很容易引起兩個錯誤:
不該返回false時,返回了false(狀態(tài)在程序中沒有被正確處理,導(dǎo)致視圖不更新)
不該返回true時,返回了true(視圖每次都更新,引起了性能問題)
何必為難自己呢?你想要讓你的程序足夠簡單,而不需要仔細(xì)考慮這些數(shù)據(jù)之間的關(guān)系。(所以,想要讓程序變得簡單,一定要設(shè)計(jì)好數(shù)據(jù)結(jié)構(gòu))
問題2:高度耦合的父子組件應(yīng)用普遍都是耦合度越低越好(組件之間要盡可能的不互相依賴)。父組件不應(yīng)該試圖去理解子組件是如何運(yùn)行的。這允許你修改子組件的行為,而父組件不需要知道更改(假定子組件的PropTypes不變)。這同樣意味著子組件可以獨(dú)立運(yùn)行,而不需要父組件嚴(yán)格的控制它的行為。
規(guī)范化你的數(shù)據(jù)結(jié)構(gòu)通過規(guī)范化你的數(shù)據(jù)結(jié)構(gòu),你可以很方便的只通過判斷引用是否更改來判斷視圖是否需要更新。
const state = { items: [ { id: 5, description: "some really cool item", // interaction now lives on the item itself interaction: { isSelected: true } } ] };
這樣的數(shù)據(jù)結(jié)構(gòu)讓你在shouldComponentUpdate函數(shù)中的更新檢測更加簡單。
import React, {Component, PropTypes} from "react"; class List extends Component { propTypes = { items: PropTypes.array.isRequired } shouldComponentUpdate (nextProps) { // so easy return isObjectEqual(this.props, nextProps); } render() {{this.props.items.map(item => { return (} }- ); })}
如果你想要更新其中的一個數(shù)據(jù),比如interaction,你只需要更新整個對象的引用就可以了。
// redux reducer export default (state, action) => { if(action.type === "ITEM_SELECT") { const {itemId} = action; const items = state.items.map(item => { if(item.id !== itemId) { return item; } // changing the reference to the whole object return { ...item, interaction: { isSelected: true } } }); return { ...state, items }; } return state; };引用檢查和動態(tài)props
先看一個創(chuàng)建動態(tài)props的例子
class Foo extends React.Component { render() { const {items} = this.props; // this object will have a new reference every time const newData = { hello: "world" }; return- } } class Item extends React.Component { // this will always return true as `data` will have a new reference every time // even if the objects have the same value shouldComponentUpdate(nextProps) { return isObjectEqual(this.props, nextProps); } }
通常我們不在組件中創(chuàng)建新的props,只是將它傳遞下去。
然而下面這種內(nèi)部循環(huán)的方式卻越來越普遍了:
class List extends React.Component { render() { const {items} = this.props;{items.map((item, index) => { // this object will have a new reference every time const newData = { hello: "world", isFirst: index === 0 }; return} }- })}
這是在創(chuàng)建函數(shù)中經(jīng)常使用的。
import myActionCreator from "./my-action-creator"; class List extends React.Component { render() { const {items, dispatch} = this.props;解決這個問題的策略{items.map(item => { // this function will have a new reference every time const callback = () => { dispatch(myActionCreator(item)); } return} }- })}
避免在組件內(nèi)部創(chuàng)建動態(tài)props(改善你的數(shù)據(jù)結(jié)構(gòu),使props可以被直接用來傳遞)
將動態(tài)props當(dāng)做滿足===不等式的類型傳遞(eg: Bool, Number, String)
const bool1 = true; const bool2 = true; bool1 === bool2; // true const string1 = "hello"; const string2 = "hello"; string1 === string2; // true
如果你真的需要傳遞一個動態(tài)對象,你可以傳遞一個對象的字符串表示,并且這個字符串應(yīng)當(dāng)可以在子組件中重新解讀為相應(yīng)的對象。
render() { const {items} = this.props;特例:函數(shù){items.map(item => { // will have a new reference every time const bad = { id: item.id, type: item.type }; // equal values will satify strict equality "===" const good = `${item.id}::${item.type}`; return}- })}
盡量不要傳遞函數(shù)。在子組件需要時才去觸發(fā)相應(yīng)的actions。這樣做還有一個好處是將業(yè)務(wù)邏輯與組件分離開來。
忽略shouldComponentUpdate函數(shù)中對functions的檢查,因?yàn)槲覀儫o法知曉函數(shù)的值是否發(fā)生改變。
創(chuàng)建一個不可變數(shù)據(jù)與函數(shù)的映射。你可以在執(zhí)行componentWillReveiveProps函數(shù)時,把這個映射放到state中。這樣的話每次render時將不會得到一個新的引用,便于執(zhí)行在shouldComponentUpdate時的引用檢查。這個方法比較麻煩,因?yàn)樾枰S護(hù)和更新函數(shù)列表。
創(chuàng)建一個有正確this綁定的中間組件。這樣也并不理想,因?yàn)樵诮M件的層次結(jié)構(gòu)中引入了冗余層。(實(shí)際上作者的意思是將函數(shù)的定義從render函數(shù)中移出,這樣每次的render就不會創(chuàng)建新的引用了)
避免每次執(zhí)行render函數(shù)時,都創(chuàng)建一個新的函數(shù)。
關(guān)于第四點(diǎn)的例子:
// introduce another layer "ListItem"工具class ListItem extends React.Component { // this will always have the correct this binding as it is tied to the instance // thanks es7! const callback = () => { dispatch(doSomething(item)); } render() { return
// you can create the correct this bindings in here - } }
上面列出的所有規(guī)則和技術(shù)都是通過使用性能測量工具發(fā)現(xiàn)的。 使用工具將幫助你找到應(yīng)用程序中特定的性能問題。
console.time這個工具相當(dāng)簡單。
開始計(jì)時
程序運(yùn)行
結(jié)束計(jì)時
一個很棒的方式是用Redux的中間件來測試性能。
export default store => next => action => { console.time(action.type); // `next` is a function that takes an "action" and sends it through to the "reducers" // this will result in a re-render of your application const result = next(action); // how long did the render take? console.timeEnd(action.type); return result; };
用這個方法,你可以記錄每個操作及其在應(yīng)用程序中渲染所花費(fèi)的時間。 你可以快速查看是哪些操作需要耗費(fèi)很多時間來執(zhí)行,這給我們提供了解決性能問題的一個起點(diǎn)。 有了這個時間值,還有助于我們查看我們對代碼的更改對應(yīng)用程序產(chǎn)生的影響。
React.perf這個工具跟console.time用起來很像,但是它是專門用來檢測React應(yīng)用性能的。
Perf.start
程序運(yùn)行
Perf.stop
依然是用Redux的中間件舉個例子
import Perf from "react-addons-perf"; export default store => next => action => { const key = `performance:${action.type}`; Perf.start(); // will re-render the application with new state const result = next(action); Perf.stop(); console.group(key); console.info("wasted"); Perf.printWasted(); // any other Perf measurements you are interested in console.groupEnd(key); return result; };
與console.time方法類似,您可以查看每個操作的表現(xiàn)數(shù)據(jù)。 有關(guān)React性能插件的更多信息,請參閱此處
瀏覽器開發(fā)者工具CPU分析器的Flame圖也有助于在應(yīng)用程序中查找性能問題。
Flame圖顯示性能展示文件中每毫秒代碼的JavaScript堆棧的狀態(tài)。 這給你一個方法來確切地知道哪個函數(shù)在記錄期間的哪個點(diǎn)執(zhí)行,運(yùn)行了多長時間,以及是從哪里被調(diào)用的 - Mozilla
Firefox: see here
Chrome: see here
感謝閱讀以及一切能讓React應(yīng)用性能提高的方式!
作者的補(bǔ)充:在檢查每個子組件的列表組件上使用shouldComponentUpdate(),并不是非常有用。
當(dāng)你有很多大列表的時候,這個方法是很有用的。能夠完全跳過列表的重新渲染時一個巨大的勝利。但是如果你的應(yīng)用中只有一個大列表,那么這樣做其實(shí)沒有任何效果,因?yàn)槟愕娜魏尾僮鞫际腔谶@個列表的,意味著列表中的數(shù)據(jù)肯定會有所改變,那么你完全可以跳過對更新條件的檢查。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/81538.html
摘要:譯文地址譯唯快不破應(yīng)用的個優(yōu)化步驟前端的逆襲知乎專欄原文地址時過境遷,應(yīng)用比以往任何時候都更具交互性。使用負(fù)載均衡方案我們在之前討論緩存的時候簡要提到了內(nèi)容分發(fā)網(wǎng)絡(luò)。換句話說,元素的串形訪問會削弱負(fù)載均衡器以最佳形式 歡迎關(guān)注知乎專欄 —— 前端的逆襲歡迎關(guān)注我的博客,知乎,GitHub。 譯文地址:【譯】唯快不破:Web 應(yīng)用的 13 個優(yōu)化步驟 - 前端的逆襲 - 知乎專欄原文地...
摘要:雖然有著各種各樣的不同,但是相同的是,他們前端優(yōu)化不完全指南前端掘金篇幅可能有點(diǎn)長,我想先聊一聊閱讀的方式,我希望你閱讀的時候,能夠把我當(dāng)作你的競爭對手,你的夢想是超越我。 如何提升頁面渲染效率 - 前端 - 掘金Web頁面的性能 我們每天都會瀏覽很多的Web頁面,使用很多基于Web的應(yīng)用。這些站點(diǎn)看起來既不一樣,用途也都各有不同,有在線視頻,Social Media,新聞,郵件客戶端...
摘要:我的教程可能也會幫你一把其他的二分法展示型組件和容器型組件這種分類并非十分嚴(yán)格,這是按照它們的目的進(jìn)行分類。在我看來,展示型組件往往是無狀態(tài)的純函數(shù)組件,容器型組件往往是有狀態(tài)的純類組件。不要把展示型組件和容器型組件的劃分當(dāng)成教條。 本文譯自Presentational and Container Components,文章的作者是Dan Abramov,他同時也是Redux和Crea...
摘要:自己英語一般,水平有限,獻(xiàn)上原文地址,還有我翻譯的中文地址,歡迎大家勘誤下面是自己的一點(diǎn)感想先說一下,我們知道,前端優(yōu)化有這么幾步,第一步首先呢我們知道,一個應(yīng)用要依賴好多條文件,而瀏覽器加載完一條,要執(zhí)行完這條才加載下一條,所以呢,就很慢 自己英語一般,水平有限,獻(xiàn)上原文地址,還有我翻譯的中文地址,歡迎大家勘誤 下面是自己的一點(diǎn)感想 先說一下webpack,我們知道,前端優(yōu)化有這么幾...
摘要:對此沒有任何限制,它不關(guān)心這個。一種控制變化的辦法是不可改變的,持久化的數(shù)據(jù)結(jié)構(gòu)。總結(jié)檢測變化時開發(fā)中的核心問題,而框架們以各種方式解決這個問題。因?yàn)榻M件內(nèi)的變化是不被允許的。 AngularJS:臟檢查 我不知道什么更新了,所以當(dāng)更新的時候,我只能檢查所有的東西。 AngularJS 類似于 Ember,當(dāng)狀態(tài)改變的時候,必須人工去處理。但不同的是,AngularJS 從不同的角度來...
閱讀 1648·2019-08-30 15:55
閱讀 973·2019-08-30 15:44
閱讀 866·2019-08-30 10:48
閱讀 2025·2019-08-29 13:42
閱讀 3179·2019-08-29 11:16
閱讀 1235·2019-08-29 11:09
閱讀 2053·2019-08-26 11:46
閱讀 611·2019-08-26 11:44