Mobx 與 Redux 的效能對比

李熠發表於2018-12-17

在本文中你將看到我最終得出的結論是 Mobx 的效能優於 Redux。但很明顯這樣的結論是片面的,甚至是有失偏頗的,因為我只選取了一個的場景對兩者進行測試。可能真實的情況恰恰相反,Mobx 僅僅在我測試的這個場景中優於 Redux,但是在我所有沒有測試到的場景中都劣於 Redux,這都是有可能的。效能跑分這類東西從來都不要放在心上,「魯大師」不也是被戲稱為「娛樂大師」嘛。

本文的重點不在於讓兩者拼個你死我活,而是在對比效能的過程中探索優劣可能是由什麼原因造成的,並且我們能從中學習到什麼

退一萬步說,即使 Redux 效能確實略遜一籌,也無傷大雅。當我們在評價一個框架,或者在為產品做技術選型時,效能只是其中的一個方面。比如 Redux 天生的 event sourcing 機制能夠幫助我們方便的回溯狀態,如果你的產品裡需要這樣的業務場景,那麼 Redux 當然是不二之選。通常在低於某個閾值下效能不會出現大的差別。

和誰比,怎麼比

讓我們從一個 stackoverflow 上關於 Mobx 的有趣的效能問題開始

提問者做了一個測試,往observable.array裝飾過的陣列(Mobx 自己的資料結構)中push200個元素,計算總共花費的時間,並且和原生的操作進行能比較。結果是使用 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 {
  @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 的開發工具我們就能看出端倪,這是執行中的指令碼的執行情況:

Mobx 與 Redux 的效能對比

注意下方原始碼中最耗時的可以追溯的Event操作,追溯到原始碼中,我們能夠看到它的呼叫棧本質上來自dispatch

Mobx 與 Redux 的效能對比

也就是說,我們有理由懷疑,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

Mobx 與 Redux 的效能對比

這份測試的原始碼位於 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>
  else
    return <div>{props.person.fullName}</div>
});
複製程式碼

從程式碼中我們得到的依賴關係如下:

Mobx 與 Redux 的效能對比

而實際上對於 Mobx 來說它會簡化為

Mobx 與 Redux 的效能對比

這樣自然就減少了非常多的計算量

對於我個人而言,我作者闡述的優化沒有太多感覺。主要我沒有做過這方面的實踐,也沒有考慮過這類方案。所以不確定它究竟能帶來多大的提升,希望在今後工作中能借鑑到這個思路

結束

就像開頭說的,這篇文章只是想起一個拋磚引玉的作用,只是對效能比較的驚鴻一瞥。另外我對在文中所描述的專案場景中採用 Mobx 的技術仍然採取保留意見,直覺這樣的效率仍然不高,將繼續探索更有效的方式

參考資料


本文同時也釋出在我個人的知乎前端專欄,歡迎大家關注


這篇文章寫的並不滿意,有失水準

相關文章