Mobx 与 Redux 的性能对比
在本文中你將看到我最終得出的結論是 Mobx 的性能優(yōu)于 Redux。但很明顯這樣的結論是片面的,甚至是有失偏頗的,因為我只選取了一個的場景對兩者進行測試??赡苷鎸嵉那闆r恰恰相反,Mobx 僅僅在我測試的這個場景中優(yōu)于 Redux,但是在我所有沒有測試到的場景中都劣于 Redux,這都是有可能的。性能跑分這類東西從來都不要放在心上,「魯大師」不也是被戲稱為「娛樂大師」嘛。
本文的重點不在于讓兩者拼個你死我活,而是在對比性能的過程中探索優(yōu)劣可能是由什么原因造成的,并且我們能從中學習到什么
退一萬步說,即使 Redux 性能確實略遜一籌,也無傷大雅。當我們在評價一個框架,或者在為產品做技術選型時,性能只是其中的一個方面。比如 Redux 天生的 event sourcing 機制能夠幫助我們方便的回溯狀態(tài),如果你的產品里需要這樣的業(yè)務場景,那么 Redux 當然是不二之選。通常在低于某個閾值下性能不會出現(xiàn)大的差別。
和誰比,怎么比
讓我們從一個 stackoverflow 上關于 Mobx 的有趣的性能問題開始
提問者做了一個測試,往observable.array裝飾過的數(shù)組(Mobx 自己的數(shù)據(jù)結構)中push200個元素,計算總共花費的時間,并且和原生的操作進行能比較。結果是使用 Mobx 的方式一共花費了 120ms, 而原生的操作只花費了不到 1ms。這是不是說明了 Mobx 性能非常糟糕?
理論上來說提問者的測試方法沒有錯,測試的結果也是正確的。但問題在于單純數(shù)值上的對比是有失公允的,雖然原生數(shù)組push方法更快,但是它無法提供單向數(shù)據(jù)流、無法提供狀態(tài)管理不是?同時 Mobx 也能與React 進行配合優(yōu)化組件的渲染。所以我們不能僅僅考量數(shù)值上的大小,還要考慮整體利益的得失。Mobx 在這項操作上慢了 120 倍,首先 120ms 的差距用戶幾乎是感知不到的,其次它換來的是給我們開發(fā)項目帶來便利,為以后的維護節(jié)省成本,要知道這些花費可是按照人月計算的。
在我做優(yōu)化工作的早期,我習慣于使用工程上的指標,比如 DOMContentLoaded 時間,onLoad 時間,軟性一點的是 Speed Index。但目前我更傾向于使用業(yè)務性質的指標,因為你要想清除一個問題是,工程的指標真的和業(yè)務指標正相關嗎?如果 onLoad 時間邊長,bounce rate 就真的會升高嗎?理論上是,但并不一定,相反如果你頑皮一點,你完全能夠做到讓 onLoad 的時間邊長,但是 bounce rate 下降,只要保證 above fold content 足夠快和可用就好了
說到底技術還是為業(yè)務服務的。最后以一篇閱讀到的論文Seven Rules of Thumb for Web Site Experimenters上的一個例子來結束這個小節(jié)。簡單來說我只想強調兩點:1) 不要盲目的、絕對的衡量性能的好壞;2) 多從業(yè)務出發(fā)考慮問題
At Bing, we use multiple performance metrics for diagnostics, but our key time-related metric is Time-To-Success (TTS) [24], which side-steps the measurement issues. For a search engine, our goal is to allow users to complete a task faster. For clickable elements, a user clicking faster on a result from which they do not come back for at least 30 seconds is considered a successful click. TTS as a metric captures perceived performance well: if it improves, then important areas of the pages are rendering faster so that users can interpret the page and click faster. This relatively simple metric does not suffer from heuristics needed for many performance metrics. It is highly robust to changes, and very sensitive. Its main deficiency is that it only works for clickable elements. For queries where the SERP has the answer (e.g., for “time” query), users can be satisfied and abandon the page without clicking.
性能對比
為什么需要進行比較是因為我在為下一個項目尋找技術選型。在新的項目中有一個重要的用戶場景類似于 Photoshop,屏幕中央有很大一塊區(qū)域用于拖拽和擺放物品。當某個物品被選中之后,四周的屬性面板現(xiàn)實該物品的各種相關屬性,當物品在實時被拖動時,面板的顯示內容也要實時進行修改。
這個場景可以抽象為:多個對象訂閱同一個對象的屬性并且展示。我分別使用 Mobx 和 Redux 通過實現(xiàn)一個實時的顯示的秒表來模擬這個場景
我一直反對在文章中貼出整段整段的代碼,但是這次沒有辦法,為了保證閱讀的完整性,似乎沒有一部分的代碼是可以省略的,于是用兩個框架寫的版本都完整的貼出來
Mobx 版本:
class StopWatch {@observablecurrentTimestamp = 0;@actionupdateCurrentTimestamp = value => {this.currentTimestamp = value;}; }const stopWatch = new StopWatch();@inject("store") @observer class StopWatchApp extends React.Component {constructor(props) {super(props);const stopWatch = this.props.store;setInterval(() => stopWatch.updateCurrentTimestamp(Date.now()));}render() {const stopWatch = this.props.store;return <div>{stopWatch.currentTimestamp}</div>;} }ReactDOM.render(<Provider store={stopWatch}><div><StopWatchApp /></div></Provider>,document.querySelector("#app") ); 復制代碼Redux 版本:
const UPDATE_ACTION = "UPDATE_ACTION";const createUpdateAction = () => ({type: UPDATE_ACTION });const stopWatch = function(initialState = {currentTimestamp: 0},action ) {switch (action.type) {case UPDATE_ACTION:initialState.currentTimestamp = Date.now();return Object.assign({}, initialState);default:return initialState;} };const store = createStore(combineReducers({stopWatch}) );class StopWatch extends React.Component {constructor(props) {super(props);const { update } = this.props;setInterval(update);}render() {const { currentTimestamp } = this.props;return <div>{currentTimestamp}</div>;} }const WrappedStopWatch = connect(function mapStateToProps(state, props) {const {stopWatch: { currentTimestamp }} = state;return {currentTimestamp};},function(dispatch) {return {update: () => {dispatch(createUpdateAction());}};} )(StopWatch);ReactDOM.render(<Provider store={store}><div><WrappedStopWatch /></div></Provider>,document.querySelector("#app") ); 復制代碼注意在上面的 Redux 版本代碼中,每一個 StopWatch 直接訂閱 store 中的 currentTimestamp 狀態(tài)。在后面我們會嘗試另一種方式
如果你分別運行這兩個版本的代碼,你不會感受到任何的差異。但是如果我們把需要展示的 Mobx 中最終渲染的 <StopWatchApp /> 實例和 Redux 中最終渲染的 <WrappedStopWatch /> 實例擴展為 20 個(這里也就有了 20 次對 store 狀態(tài)的訂閱):
ReactDOM.render(<Provider store={store}><div><WrappedStopWatch /><WrappedStopWatch /><WrappedStopWatch /><WrappedStopWatch /><WrappedStopWatch />// ...省略后面的15個</div></Provider>,document.querySelector("#app") ); 復制代碼你會感受到 Redux 明顯出現(xiàn)了卡頓(通過肉眼就能觀察出來,這里就不需要使用精確的時間顯示差別了),或者說變化速率明顯比 Mobx 版本更慢。這里就不貼視頻或者是 gif 圖了。各位運行代碼就能一目了然
為什么呢,通過 Chrome 的開發(fā)工具我們就能看出端倪,這是運行中的腳本的執(zhí)行情況:
注意下方源碼中最耗時的可以追溯的Event操作,追溯到源碼中,我們能夠看到它的調用棧本質上來自dispatch:
也就是說,我們有理由懷疑,Redux 的 dispatch 會造成性能的損耗(該死,這可是最核心的機制)。我們不妨先做一個假設:在上面的代碼中,因為我們使用了獨立訂閱 store 的 20 個組件,間接使用了disaptch,最終導致性能下降。接下來我們要驗證這個假設是否正確,原理非常簡單,我們實現(xiàn)相同的效果,即同時在頁面上顯示20個秒表,但是只使用一個訂閱——我們使用一個父容器訂閱 store,然后把狀態(tài)傳遞給子組件。store 部分不用修改,組件部分修改如下:
const StopWatch = ({ currentTimestamp }) => {return <div>{currentTimestamp}</div>; };class Container extends React.Component {constructor(props) {super(props);const { update } = this.props;setInterval(update);}render() {const { currentTimestamp } = this.props;return (<div><StopWatch currentTimestamp={currentTimestamp} />// 省略剩下的 19 個</div>);} }const WrappedContainer = connect(function mapStateToProps(state, props) {const {stopWatch: { currentTimestamp }} = state;return {currentTimestamp};},function(dispatch) {return {update: () => {dispatch(createUpdateAction());}};} )(Container);ReactDOM.render(<Provider store={store}><div><WrappedContainer /></div></Provider>,document.querySelector("#app") ); 復制代碼這段代碼驗證了我們的想法,修改之后程序變得健步如飛了,達到了和 Mobx 相同的顯示速率。這也驗證了我們的假設,dispatch確實會帶來性能上的損失,但可怕的事情是dispatch是 Redux 事件機制的意志體現(xiàn)。這里我們不繼續(xù)探究為什么dispatch的變慢的原因
但切記, 通過父容器渲染這不是常規(guī)的優(yōu)化方案
在差不多在一年前的文章「React + Redux 性能優(yōu)化(一):理論篇」 里,我提到過由父容器統(tǒng)一渲染列表其實是下下策。因為 immutable data 的關系,一旦列表中某一項數(shù)據(jù)內容發(fā)生了渲染,會導致整個列表都會被重新渲染,包括那些沒有被修改的
我給出的建議是,當你在渲染一個列表時,將列表的數(shù)據(jù)結構劃分為兩個部分,id列表和項目字典:父容器只根據(jù)id列表負責渲染每一項的外層容器,而每一項的具體內容,則是每一個項目組件直接訪問 store 獲得:
class App extends Component {render() {const { ids } = this.props;return (<div>{ids.map(id => {return <Item key={id} id={id} />;})}</div>);} } 復制代碼另一個關于 Mobx 與 Redux 性能對比測試的例子是來自于 Mobx 的作者 Michel Weststrate(好吧,這聽上去就有失公允了),來自他的這篇 twitter
這份測試的源碼位于 github.com/mweststrate…
測試中展示了在 Mobx 和 Redux 同一個操作下(在 todo mvc 中修改一個 todo 或者是新增一個 todo)所需要的時間(另一個變量是 todo 的數(shù)量)。 從圖中可以看出,無論是哪一種情況,Mobx 花費的時間最少。
Mobx 為什么會快
這個問題 Mobx 的作者在 Becoming fully reactive: an in-depth explanation of MobX 這篇文章里已經解釋的很清楚了,這里我們簡單摘抄幾點
以 Redux 應用為例,你需要使用訂閱機制解決數(shù)據(jù)同步的問題,比如視圖中的數(shù)據(jù)會出現(xiàn)與 store(或者是 selector)中數(shù)據(jù)不一致的情況。但是隨著應用的增長,管理訂閱會變得越來約復雜,比如你有可能訂閱了已經不再使用的數(shù)據(jù),或者過度訂閱了你不需要的數(shù)據(jù),或者忘記訂閱了你需要的數(shù)據(jù)。在 React 中,過度的訂閱會造成組件沒有意義的重復渲染。注意即使你的訂閱的是只在特定條件下需要使用的數(shù)據(jù),也算過度訂閱
所以 Mobx 背后非常重要的一個設計哲學是:一個運行時決定的最小訂閱子集(A minimal, consistent set of subscriptions can only be achieved if subscriptions are determined at run-time.)
辦法非常的簡單,所有的數(shù)據(jù)都不會被緩存,而是統(tǒng)統(tǒng)通過派生(derive)計算出來(如果你了解 Mobx 你應該知道 derivation 的概念,它代指 computed value 和 reactions)。但是這樣代價不會很大嗎?不,相反它非常高效。 Mobx 并不會計算所有的派生值,而是計算那些目前處于 observable 狀態(tài)中的(或者更通俗的理解是當前被使用的,或者說是可見的)。
舉個例子,比如下面的代碼:
class Person {@observable firstName = "Michel";@observable lastName = "Weststrate";@observable nickName;@computed get fullName() {return this.firstName + " " + this.lastName;} }// Example React component that observes state const profileView = observer(props => {if (props.person.nickName)return <div>{props.person.nickName}</div>elsereturn <div>{props.person.fullName}</div> }); 復制代碼從代碼中我們得到的依賴關系如下:
而實際上對于 Mobx 來說它會簡化為
這樣自然就減少了非常多的計算量
對于我個人而言,我作者闡述的優(yōu)化沒有太多感覺。主要我沒有做過這方面的實踐,也沒有考慮過這類方案。所以不確定它究竟能帶來多大的提升,希望在今后工作中能借鑒到這個思路
結束
就像開頭說的,這篇文章只是想起一個拋磚引玉的作用,只是對性能比較的驚鴻一瞥。另外我對在文中所描述的項目場景中采用 Mobx 的技術仍然采取保留意見,直覺這樣的效率仍然不高,將繼續(xù)探索更有效的方式
參考資料
- Seven Rules of Thumb for Web Site Experimenters
- Becoming fully reactive: an in-depth explanation of MobX
本文同時也發(fā)布在我個人的知乎前端專欄,歡迎大家關注
這篇文章寫的并不滿意,有失水準
總結
以上是生活随笔為你收集整理的Mobx 与 Redux 的性能对比的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 8支团队正在努力构建下一代Ethereu
- 下一篇: jvm(Java virtual mac