在本文中你將看到我最終得出的結論是 Mobx 的性能優于 Redux。但很明顯這樣的結論是片面的,甚至是有失偏頗的,因為我只選取了一個的場景對兩者進行測試。可能真實的情況恰恰相反,Mobx 僅僅在我測試的這個場景中優于 Redux,但是在我所有沒有測試到的場景中都劣于 Redux,這都是有可能的。性能跑分這類東西從來都不要放在心上,「魯大師」不也是被戲稱為「娛樂大師」嘛。
本文的重點不在于讓兩者拼個你死我活,而是在對比性能的過程中探索優劣可能是由什么原因造成的,并且我們能從中學習到什么
退一萬步說,即使 Redux 性能確實略遜一籌,也無傷大雅。當我們在評價一個框架,或者在為產品做技術選型時,性能只是其中的一個方面。比如 Redux 天生的 event sourcing 機制能夠幫助我們方便的回溯狀態,如果你的產品里需要這樣的業務場景,那么 Redux 當然是不二之選。通常在低于某個閾值下性能不會出現大的差別。
和誰比,怎么比
讓我們從一個 stackoverflow 上關于 Mobx 的有趣的性能問題開始
提問者做了一個測試,往observable.array
裝飾過的數組(Mobx 自己的數據結構)中push
200個元素,計算總共花費的時間,并且和原生的操作進行能比較。結果是使用 Mobx 的方式一共花費了 120ms, 而原生的操作只花費了不到 1ms。這是不是說明了 Mobx 性能非常糟糕?
理論上來說提問者的測試方法沒有錯,測試的結果也是正確的。但問題在于單純數值上的對比是有失公允的,雖然原生數組push
方法更快,但是它無法提供單向數據流、無法提供狀態管理不是?同時 Mobx 也能與React 進行配合優化組件的渲染。所以我們不能僅僅考量數值上的大小,還要考慮整體利益的得失。Mobx 在這項操作上慢了 120 倍,首先 120ms 的差距用戶幾乎是感知不到的,其次它換來的是給我們開發項目帶來便利,為以后的維護節省成本,要知道這些花費可是按照人月計算的。
在我做優化工作的早期,我習慣于使用工程上的指標,比如 DOMContentLoaded 時間,onLoad 時間,軟性一點的是 Speed Index。但目前我更傾向于使用業務性質的指標,因為你要想清除一個問題是,工程的指標真的和業務指標正相關嗎?如果 onLoad 時間邊長,bounce rate 就真的會升高嗎?理論上是,但并不一定,相反如果你頑皮一點,你完全能夠做到讓 onLoad 的時間邊長,但是 bounce rate 下降,只要保證 above fold content 足夠快和可用就好了
說到底技術還是為業務服務的。最后以一篇閱讀到的論文Seven Rules of Thumb for Web Site Experimenters上的一個例子來結束這個小節。簡單來說我只想強調兩點:1) 不要盲目的、絕對的衡量性能的好壞;2) 多從業務出發考慮問題
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,屏幕中央有很大一塊區域用于拖拽和擺放物品。當某個物品被選中之后,四周的屬性面板現實該物品的各種相關屬性,當物品在實時被拖動時,面板的顯示內容也要實時進行修改。
這個場景可以抽象為:多個對象訂閱同一個對象的屬性并且展示。我分別使用 Mobx 和 Redux 通過實現一個實時的顯示的秒表來模擬這個場景
我一直反對在文章中貼出整段整段的代碼,但是這次沒有辦法,為了保證閱讀的完整性,似乎沒有一部分的代碼是可以省略的,于是用兩個框架寫的版本都完整的貼出來
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 狀態。在后面我們會嘗試另一種方式
如果你分別運行這兩個版本的代碼,你不會感受到任何的差異。但是如果我們把需要展示的 Mobx 中最終渲染的 <StopWatchApp />
實例和 Redux 中最終渲染的 <WrappedStopWatch />
實例擴展為 20 個(這里也就有了 20 次對 store 狀態的訂閱):
ReactDOM.render(<Provider store={store}><div><WrappedStopWatch /><WrappedStopWatch /><WrappedStopWatch /><WrappedStopWatch /><WrappedStopWatch />// ...省略后面的15個</div></Provider>,document.querySelector("#app")
);
復制代碼
你會感受到 Redux 明顯出現了卡頓(通過肉眼就能觀察出來,這里就不需要使用精確的時間顯示差別了),或者說變化速率明顯比 Mobx 版本更慢。這里就不貼視頻或者是 gif 圖了。各位運行代碼就能一目了然
為什么呢,通過 Chrome 的開發工具我們就能看出端倪,這是運行中的腳本的執行情況:
注意下方源碼中最耗時的可以追溯的Event
操作,追溯到源碼中,我們能夠看到它的調用棧本質上來自dispatch
:
也就是說,我們有理由懷疑,Redux 的 dispatch
會造成性能的損耗(該死,這可是最核心的機制)。我們不妨先做一個假設:在上面的代碼中,因為我們使用了獨立訂閱 store 的 20 個組件,間接使用了disaptch
,最終導致性能下降。接下來我們要驗證這個假設是否正確,原理非常簡單,我們實現相同的效果,即同時在頁面上顯示20個秒表,但是只使用一個訂閱——我們使用一個父容器訂閱 store,然后把狀態傳遞給子組件。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 事件機制的意志體現。這里我們不繼續探究為什么dispatch
的變慢的原因
但切記, 通過父容器渲染這不是常規的優化方案
在差不多在一年前的文章「React + Redux 性能優化(一):理論篇」 里,我提到過由父容器統一渲染列表其實是下下策。因為 immutable data 的關系,一旦列表中某一項數據內容發生了渲染,會導致整個列表都會被重新渲染,包括那些沒有被修改的
我給出的建議是,當你在渲染一個列表時,將列表的數據結構劃分為兩個部分,id列表和項目字典:父容器只根據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 的數量)。 從圖中可以看出,無論是哪一種情況,Mobx 花費的時間最少。
Mobx 為什么會快
這個問題 Mobx 的作者在 Becoming fully reactive: an in-depth explanation of MobX 這篇文章里已經解釋的很清楚了,這里我們簡單摘抄幾點
以 Redux 應用為例,你需要使用訂閱機制解決數據同步的問題,比如視圖中的數據會出現與 store(或者是 selector)中數據不一致的情況。但是隨著應用的增長,管理訂閱會變得越來約復雜,比如你有可能訂閱了已經不再使用的數據,或者過度訂閱了你不需要的數據,或者忘記訂閱了你需要的數據。在 React 中,過度的訂閱會造成組件沒有意義的重復渲染。注意即使你的訂閱的是只在特定條件下需要使用的數據,也算過度訂閱
所以 Mobx 背后非常重要的一個設計哲學是:一個運行時決定的最小訂閱子集(A minimal, consistent set of subscriptions can only be achieved if subscriptions are determined at run-time.)
辦法非常的簡單,所有的數據都不會被緩存,而是統統通過派生(derive)計算出來(如果你了解 Mobx 你應該知道 derivation 的概念,它代指 computed value 和 reactions)。但是這樣代價不會很大嗎?不,相反它非常高效。 Mobx 并不會計算所有的派生值,而是計算那些目前處于 observable 狀態中的(或者更通俗的理解是當前被使用的,或者說是可見的)。
舉個例子,比如下面的代碼:
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 來說它會簡化為
這樣自然就減少了非常多的計算量
對于我個人而言,我作者闡述的優化沒有太多感覺。主要我沒有做過這方面的實踐,也沒有考慮過這類方案。所以不確定它究竟能帶來多大的提升,希望在今后工作中能借鑒到這個思路
結束
就像開頭說的,這篇文章只是想起一個拋磚引玉的作用,只是對性能比較的驚鴻一瞥。另外我對在文中所描述的項目場景中采用 Mobx 的技術仍然采取保留意見,直覺這樣的效率仍然不高,將繼續探索更有效的方式
參考資料
- Seven Rules of Thumb for Web Site Experimenters
- Becoming fully reactive: an in-depth explanation of MobX
本文同時也發布在我個人的知乎前端專欄,歡迎大家關注
這篇文章寫的并不滿意,有失水準