在本文中你將看到我最終得出的結論是 Mobx 的性能優於 Redux。但很明顯這樣的結論是片面的,甚至是有失偏頗的,由於我只選取了一個的場景對二者進行測試。可能真實的狀況偏偏相反,Mobx 僅僅在我測試的這個場景中優於 Redux,可是在我全部沒有測試到的場景中都劣於 Redux,這都是有可能的。性能跑分這類東西曆來都不要放在心上,「魯大師」不也是被戲稱爲「娛樂大師」嘛。javascript
本文的重點不在於讓二者拼個你死我活,而是在對比性能的過程當中探索優劣多是由什麼緣由形成的,而且咱們能從中學習到什麼前端
退一萬步說,即便 Redux 性能確實略遜一籌,也無傷大雅。當咱們在評價一個框架,或者在爲產品作技術選型時,性能只是其中的一個方面。好比 Redux 天生的 event sourcing 機制可以幫助咱們方便的回溯狀態,若是你的產品裏須要這樣的業務場景,那麼 Redux 固然是不二之選。一般在低於某個閾值下性能不會出現大的差異。java
讓咱們從一個 stackoverflow 上關於 Mobx 的有趣的性能問題開始react
提問者作了一個測試,往observable.array
裝飾過的數組(Mobx 本身的數據結構)中push
200個元素,計算總共花費的時間,而且和原生的操做進行能比較。結果是使用 Mobx 的方式一共花費了 120ms, 而原生的操做只花費了不到 1ms。這是否是說明了 Mobx 性能很是糟糕?git
理論上來講提問者的測試方法沒有錯,測試的結果也是正確的。但問題在於單純數值上的對比是有失公允的,雖然原生數組push
方法更快,可是它沒法提供單向數據流、沒法提供狀態管理不是?同時 Mobx 也能與React 進行配合優化組件的渲染。因此咱們不能僅僅考量數值上的大小,還要考慮總體利益的得失。Mobx 在這項操做上慢了 120 倍,首先 120ms 的差距用戶幾乎是感知不到的,其次它換來的是給咱們開發項目帶來便利,爲之後的維護節省成本,要知道這些花費但是按照人月計算的。github
在我作優化工做的早期,我習慣於使用工程上的指標,好比 DOMContentLoaded 時間,onLoad 時間,軟性一點的是 Speed Index。但目前我更傾向於使用業務性質的指標,由於你要想清除一個問題是,工程的指標真的和業務指標正相關嗎?若是 onLoad 時間邊長,bounce rate 就真的會升高嗎?理論上是,但並不必定,相反若是你頑皮一點,你徹底可以作到讓 onLoad 的時間邊長,可是 bounce rate 降低,只要保證 above fold content 足夠快和可用就行了redux
說到底技術仍是爲業務服務的。最後以一篇閱讀到的論文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 {
@observable
currentTimestamp = 0;
@action
updateCurrentTimestamp = 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 的做者在 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>
else
return <div>{props.person.fullName}</div>
});
複製代碼
從代碼中咱們獲得的依賴關係以下:
而實際上對於 Mobx 來講它會簡化爲
這樣天然就減小了很是多的計算量
對於我我的而言,我做者闡述的優化沒有太多感受。主要我沒有作過這方面的實踐,也沒有考慮過這類方案。因此不肯定它究竟能帶來多大的提高,但願在從此工做中能借鑑到這個思路
就像開頭說的,這篇文章只是想起一個拋磚引玉的做用,只是對性能比較的驚鴻一瞥。另外我對在文中所描述的項目場景中採用 Mobx 的技術仍然採起保留意見,直覺這樣的效率仍然不高,將繼續探索更有效的方式
本文同時也發佈在我我的的知乎前端專欄,歡迎你們關注
這篇文章寫的並不滿意,有失水準