標籤:

react.js文件體積為何如此龐大?

既然它只是實現了 UI 一層,按常理來說體積應該很小才對,為何代碼量大到 gzip 以後也有35KB+


React 文件體積為何如此龐大?我之前對這個問題也倍感困惑,在知乎尋求答案無果,遂自行解決。

首先,為什麼會得出 React 大的結論,對比幾個前端框架的 min 文件:

  • Mithril 0.2.3 19K (8K gzipped)
  • Angular 1.2.16 102K (38K gzipped)
  • Vue 1.0.8 73K (24K gzipped)
  • React 0.14.7 133K (38K gzipped)

React 作為一個 View-ViewModel 庫,相比於 Mithril,Vue 這些目的大致相同的庫,文件顯得尤為龐大,甚至比 Angular 這種全能 MVVM 框架還大。但是這就能說 React 大嗎,我認為是的,Mithril 與 React 都是基於 Virtual DOM 的實現,我覺得這很有可比性。雖然 Mithril 的真實性能大致為 React 的一半,但是代碼量卻是 React 的不到 1/10(實際上 Mithril 只有大概 2000 行代碼),Mithril 在使用了一些小技巧之後甚至性能飆升至 React 的數十倍。對 Mithril 感興趣可以看下我對 Mithril 渲染性能的分析:《ARV 渲染實現番外篇之 Mithril》

通過分析 React 的源碼及其打包過程,可以得出 React 的成分構成:

  • React: 687KB 100%
    • renderers 這部分代表了 Virtual DOM 的渲染部分
      • ReactDOM 316KB 55% 主要作用是將 Virtual DOM 渲染成真實 DOM,並進行關聯以及之後的 DOM 操作,DOM 事件處理框架
      • ReactDOMServer 9KB 1% 這部分代碼很大程度上重用 ReactDOM 所以代碼量不多
      • ReactReconciler 106KB 15% 這部分代表了 React 的差異比較以及做出 DOM 操作的調和演算法
      • ReactEvent 63KB 9%
    • ReactIsomorphic 103KB 15% 這部分是 Virtual DOM 的結構代碼,包括 ReactComponent,ReactClass,ReactElement 等的實現
    • 輔助測試代碼 48KB 7% 可能這裡有一些沒有打包的模塊,但是確實有一些如 ReactPerf,ReactDefaultPerf 等模塊是打包進去了
    • 其他依賴庫 38KB 6%

這裡分析的百分比是打包進 react.js(壓縮前)的各部分源碼文件大小(及其依賴文件)占所有打包文件大小(不包括addons以及fbjs)的百分比,跟壓縮合併後的百分比有一定差別,但是可以一定程度上代表代碼成分。從成分看來,實際上 VirtualDOM 的代碼並不多,大部分代碼都放在跟真實 DOM 相關的操作裡面,儘管從認知上來看,這些操作並不需要寫如此多的代碼。我認為導致代碼量大的原因大致有以下幾點。

注重安全性

嚴格的 Virtual DOM 檢查以及 ReactDOM 操作,包括 props 類型,經典 DOM 元素的標籤名,屬性名,CSS屬性名,標籤嵌套合法性,支持的原生 DOM 事件類型,等等都有明確的定義。React 毫不吝嗇地在所有涉及安全問題的地方使用白名單來提高安全性,這也是代碼量大的原因之一:

var HTMLDOMPropertyConfig = {
isCustomAttribute: RegExp.prototype.test.bind(
/^(data|aria)-[a-z_][a-zd_.-]*$/
),
Properties: {
/**
* Standard Properties
*/
accept: null,
acceptCharset: null,
accessKey: null,
action: null,
allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
allowTransparency: MUST_USE_ATTRIBUTE,
alt: null,
async: HAS_BOOLEAN_VALUE,
autoComplete: null,
// autoFocus is polyfilled/normalized by AutoFocusUtils
// autoFocus: HAS_BOOLEAN_VALUE,
autoPlay: HAS_BOOLEAN_VALUE,
capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
cellPadding: null,
cellSpacing: null,
charSet: MUST_USE_ATTRIBUTE,
challenge: MUST_USE_ATTRIBUTE,
checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
classID: MUST_USE_ATTRIBUTE,
// To set className on SVG elements, it"s necessary to use .setAttribute;
// this works on HTML elements too in all browsers except IE8. Conveniently,
// IE8 doesn"t support SVG and so we can simply use the attribute in
// browsers that support SVG and the property in browsers that don"t,
// regardless of whether the element is HTML or SVG.
className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,
cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
colSpan: null,
content: null,
contentEditable: null,
contextMenu: MUST_USE_ATTRIBUTE,
controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
coords: null,
crossOrigin: null,
data: null, // For `&` acts as `src`.
dateTime: MUST_USE_ATTRIBUTE,
default: HAS_BOOLEAN_VALUE,
defer: HAS_BOOLEAN_VALUE,
// ... 省略150行
}

實現同構

ReactDOM 在前端渲染需要做同構處理,即將服務端返回的第一遍渲染出來的 html DOM 綁定到前端 Virtual DOM 樹,這增加了不少 ReactDOM 和 ReactDOMServer 的共用代碼,因為必須兼顧瀏覽器端和服務端渲染的一致性。

除此之外,ReactDOMServer 即使在前端渲染不會用到,但是為了前後端使用同一份代碼,所以依然打包進了 react.js,這也增加了一些體積。

開發者友好

大量的提示文本,針對每個模塊方法的誤用或被拋棄的方法,都有具體詳細的提示,這些提示混淆壓縮後長度不變:

warning(
owner._warnedAboutRefsInRender,
"%s is accessing getDOMNode or findDOMNode inside its render(). " +
"render() should be a pure function of props and state. It should " +
"never access something that requires stale data from the previous " +
"render, such as refs. Move this logic to componentDidMount and " +
"componentDidUpdate instead.",
owner.getName() || "A component"
);

另外,喜歡使用完整的語句作為方法及屬性名,如 registrationNameDependencies,也是導致容量大的原因,因為方法名和屬性名即使經過壓縮混淆,長度也不會改變。以下是壓縮後的 React 入口模塊代碼(格式化):

"use strict";
var r = e(35),
o = e(45),
a = e(61),
i = e(23),
u = e(104),
s = {};
i(s, a),
i(s, {
findDOMNode : u("findDOMNode", "ReactDOM", "react-dom", r, r.findDOMNode),
render : u("render", "ReactDOM", "react-dom", r, r.render),
unmountComponentAtNode : u("unmountComponentAtNode", "ReactDOM", "react-dom", r, r.unmountComponentAtNode),
renderToString : u("renderToString", "ReactDOMServer", "react-dom/server", o, o.renderToString),
renderToStaticMarkup : u("renderToStaticMarkup", "ReactDOMServer", "react-dom/server", o, o.renderToStaticMarkup)
}),
s.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = r,
s.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = o,
t.exports = s

過度模塊化

react.js 裡面包含了151個模塊的定義,平均每個模塊化增加的額外代碼量:

// 模塊編譯後生成代碼
2 : [function (e, t, n) {
// 原模塊定義代碼
}, {
106 : 106,
136 : 136,
63 : 63
}
]

每個模塊增加的代碼約為45Byte,加上統一處理函數,共約為7KB的大小。雖然看起來不多,但實際上這佔了壓縮後的 react.min.js(133KB) 的 5% 的大小。至於真的需要寫那麼多個模塊嗎,我覺得是不用的,至少一個模塊裡面只有一個函數這種(如 onlyChild 模塊)是可以集中寫的。除此之外,雖然有如此龐大的模塊集合,React 的模塊間耦合還是很高,模塊間相互調用十分繁多。

其他依賴

覺得 React 大的原因除了 react.js 本身的龐大外,還有需要和 React 搭配使用的庫也有很多,包括 React 自己的 addons,封裝好的 http 請求,Flux,Redux 等框架,處理複雜狀態的 Immutable.js 等等等。另外如果要適配 IE8 還要引入一堆 polyfill 也增加了不少容量。

結論

React 確實很大,但是也並非大而無當,起碼出發點是好的。但是有沒有優化的空間呢,我認為是有的,起碼如果區分開發版和發行版能有效的去掉對用戶來說沒有用的開發者提示。

題外話

我們知道我們在程序中需要使用 react-dom.js 來使用 ReactDOM,然而實際上這個文件並不真的包含 ReactDOM 的實現,按照上面的分析 ReactDOM 是 react.js 的一部分:

React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOM;
React.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOMServer;

那麼 react-dom.js 到底幹了啥,我們來看看:

function(React) {
return React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
}

你TM逗我。其實這也可以理解,React 內部模塊的耦合決定了 ReactDOM 不可能單獨抽取出來用,react-dom.js 這個模塊文件只是給我們暴露一個入口。


謝邀,先佔位關注。

體積大是因為一個文件打包了所有依賴腳本。而react核心是virtual dom,為此需要構建一整套對h5的標籤封裝和其他輔助庫。

我這邊遇到這個問題是在react native的jsbundle中,情況是相似的。之前交流時幾位同學都建議通過webpack來處理依賴。正在處理,估計過幾天有結論。


推薦閱讀:

為什麼 React 推崇 HOC 和組合的方式,而不是繼承的方式來擴展組件?
說說對react中JSX語法的理解?
前端發展太快,有些小伙只會用react(了解api),招個jquery熟練的外包較難,如何看?
請問react中有什麼好用的ui庫嗎?
react服務端渲染如何將數據同步到客戶端?

TAG:React |