Mobx 與 Redux 的性能對比

在本文中你將看到我最終得出的結論是 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 {@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

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


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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/281258.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/281258.shtml
英文地址,請注明出處:http://en.pswp.cn/news/281258.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux lsof/netstat查看進程和端口號相關命令:

本文為博主原創&#xff0c;未經允許不得轉載&#xff1a; 在linux操作時&#xff0c;經常要查看運行的項目的進程和端口號&#xff0c;在這里總結了以下常用到的相關命令&#xff1a; 1.查看系統運行的java項目&#xff0c;并查看進程號 這個用到的命令為&#xff1a; ps -ef|…

C#高級編程9 第17章 使用VS2013-C#特性

C#高級編程9 第17章 使用VS2013 編輯定位到 如果默認勾選了這項&#xff0c;請去掉勾選&#xff0c;因為勾選之后解決方案的目錄會根據當前文件選中。 可以設置項目并行生成數 版本控制軟件設置 所有文本編輯器行號顯示 啟用編輯繼續 收集調試信息&#xff0c;將影響性能 Code …

還在手畫C#依賴關系圖嗎?快來試試這個工具吧!

還在手畫C#依賴關系圖嗎&#xff1f;快來試試這個工具吧&#xff01;筆者最近見到了一個不錯的工具&#xff0c;可以讓大家在看代碼的時候一鍵生成C#依賴的類圖。非常適合編寫文檔、查看和學習開源項目設計時使用&#xff0c;比如下方就是筆者通過這個工具生成的Microsoft.Exte…

Web服務器 - Apache配置介紹

基本語法 常量的定義與使用&#xff0c;使用關鍵詞 Define 可以定義常量&#xff0c;使用 ${} 插入常量&#xff0c;如下 語法規則說明示列Define定義常量Define SRVROOT “D:/srv/Apache24”${}使用常量ServerRoot “${SRVROOT}”/表示路徑時使用 / 而不使用 \D:/srv/Apache…

點火開關分為4個檔位,分別是off,acc,IG-on,和ST

off全車除了常火&#xff08;如應急燈&#xff0c;時鐘等的記憶功能&#xff09;外&#xff0c;均不供電。acc 是附件檔&#xff0c;部分車載附屬設備供電&#xff0c;如視聽系統&#xff0c;儀表燈&#xff0c;燈光等。也就是說&#xff0c;車停在哪里&#xff0c;發動機不轉&…

h5的formData 上傳文件及.net后臺

先來前端的代碼&#xff1a; html 代碼&#xff1a; <input type"file" id"files" value"" multiple/> js代碼&#xff1a; function init() {var ele_files document.querySelector("#files");ele_files.addEventListener(&qu…

51 Nod 1027 大數乘法【Java大數亂搞】

1027 大數乘法 基準時間限制&#xff1a;1 秒 空間限制&#xff1a;131072 KB 分值: 0 難度&#xff1a;基礎題 給出2個大整數A,B&#xff0c;計算A*B的結果。Input第1行&#xff1a;大數A 第2行&#xff1a;大數B (A,B的長度 < 1000&#xff0c;A,B > 0&#xff09; Out…

關于ASP.NET Core WebSocket實現集群的思考

前言提到WebSocket相信大家都聽說過&#xff0c;它的初衷是為了解決客戶端瀏覽器與服務端進行雙向通信&#xff0c;是在單個TCP連接上進行全雙工通訊的協議。在沒有WebSocket之前只能通過瀏覽器到服務端的請求應答模式比如輪詢&#xff0c;來實現服務端的變更響應到客戶端&…

windows環境下Apache+PHP+MySQL搭建服務器

相關文件下載 下載地址Apachehttps://www.apachehaus.com/cgi-bin/download.plxPHPhttps://windows.php.net/downloadMySQLhttps://dev.mysql.com/downloads/mysql/MySQL MySQL配置 當前使用的MySQL版本是8.0.18&#xff0c;在MySQL根目錄下新建my.ini文件&#xff0c;下面是…

angular.js國際化模塊

最近需要將一個項目轉化成英文的&#xff0c; 于是發現一個angular模塊angular-translate&#xff0c;實現如下&#xff1a; 1.安裝包 bower install angular-translate bower install angular-translate-loader-static-files //然后在頁面引用進去 <script src"/angul…

觸屏網站如何實現返回并刷新

目的 在會員中心等頁面常常會遇到進入內頁修改信息&#xff0c;返回前一個頁面需要更新信息的場景。 思路 用COOKIE記錄當前頁面是否需要刷新&#xff0c;返回之后再刷新一次頁面。 方案 下載js.cookie.js然后引入到項目中 https://github.com/js-cookie/js-cookie 先來一個最簡…

更快,更強的.NET 7 發布了

.NET Conf 2022 在昨晚(11?8?) 11 點 正式開始了&#xff0c;為期三天的會議&#xff08;11?8-10?&#xff09;&#xff0c; 圍繞 .NET 7 展開。相信各位?伙伴都已經開始安裝 .NET 7 正式版本還有以及相關的開發?具。這次 .NET 7 圍繞傳統的 C# &#xff0c;ASP.NET Core…

Web服務器 - Nginx配置介紹

nginx的配置相對簡單&#xff0c;總體來說分為5種模塊 全局塊&#xff1a;配置影響nginx全局的指令。一般有運行nginx服務器的用戶組&#xff0c;nginx進程pid存放路徑&#xff0c;日志存放路徑&#xff0c;配置文件引入&#xff0c;允許生成worker process數等。events塊&…

jvm(Java virtual machine) JVM架構解釋

2019獨角獸企業重金招聘Python工程師標準>>> JVM 架構解釋 每個Java開發者都知道通過JRE【Java運行環境】執行字節碼。 但是很多人都不知道JRE是JVM實現的事實。JVM負責執行字節碼的分析 代碼的解釋和運行。 我們應該了解JVM的架構&#xff0c;這對開發者來說是很重…

Hyper-V 嵌套虛擬化

先決條件運行 Windows Server 2016 或Windows 10 周年更新的 Hyper-V 主機。運行 Windows Server 2016 或Windows 10 周年更新的 Hyper-V VM。配置版本為 8.0 或更高的 Hyper-V VM。采用 VT-x 和 EPT 技術的 Intel 處理器&#xff08;AMD-V技術的暫時不支持&#xff09;>Set…

簡單的面試題簡解思路(搜集)

1. 統計字符串中單詞出現次數 "hi how are you i am fine thank you youtube am am "&#xff0c;統計"you"出現的次數。 方法一 : split() function wordCount(str,word){var str str || "";var word word || "";var strArr s…

WinForm(十五)窗體間通信

在很多WinForm的程序中&#xff0c;會有客戶端之間相互通信的需求&#xff0c;或服務端與客戶端通信的需求&#xff0c;這時就要用到TCP/IP的功能。在.NET中&#xff0c;主要是通過Socket來完成的&#xff0c;下面的例子是通過一個TcpListerner作為監聽&#xff0c;等待TcpClie…

905. 按奇偶排序數組

1// 905. 按奇偶排序數組 2/** 3 * param {number[]} A 4 * return {number[]} 5 */ 6var sortArrayByParity function(A) { 7 return A.filter(value > value % 2 0).concat( 8 A.filter(value > value % 2 1) 9 )10}; 轉載于:https://www.cnblogs.com/…

關于Java開發需要注意的十二點流程

1.將一些需要變動的配置寫在屬性文件中 比如&#xff0c;沒有把一些需要并發執行時使用的線程數設置成可在屬性文件中配置。那么你的程序無論在DEV環境中&#xff0c;還是TEST環境中&#xff0c;都可以順暢無阻地運行&#xff0c;但是一旦部署在PROD上&#xff0c;把它作為多線…

Unity經典游戲教程之:雪人兄弟

版權聲明&#xff1a; 本文原創發布于博客園"優夢創客"的博客空間&#xff08;網址&#xff1a;http://www.cnblogs.com/raymondking123/&#xff09;以及微信公眾號"優夢創客"&#xff08;微信號&#xff1a;unitymaker&#xff09;您可以自由轉載&#x…