React 服務端渲染緩慢原因淺析
前幾日筆者在 服務端渲染性能大亂斗:Vue, React, Preact, Rax, Marko 一文中比較了當前流行的數個前端框架服務端渲染的性能表現,下圖數值越高越好:
筆者看完這個數據對比之后不由好奇,緣何 React 服務端渲染的性能會如此之差;從設計理念的角度來看 React 本身專注于跨平臺的界面庫,其保證較好抽象層次的同時勢必會付出一定的代價,并且 非死book 在生產環境中并未大規模應用服務端渲染,也就未花費過多的精力來優化服務端渲染的性能。筆者也對比了下 React 與 Preact 有關服務端渲染的實現代碼,確實高度的抽象需要額外的代碼邏輯與對象創建,React 本身并沒有冗余的部分,只是單純地大量的毫秒級別額外對象操作的耗時的累加導致了最后性能表現的巨大差異。我們首先看下 Preact 的 renderToString 的函數實現,其緊耦合于 DOM 環境,以較低的抽象程度換取較少的代碼實現:
/** The default export is an alias of `render()`. */
export default function renderToString(vnode, context, opts, inner, isSvgMode) {
// 獲取節點屬性
let { nodeName, attributes, children } = vnode || EMPTY,
isComponent = false;
context = context || {};
opts = opts || {};
let pretty = opts.pretty,
indentChar = typeof pretty==='string' ? pretty : '\t';
if (vnode==null) {
return '';
}
// 字符串類型則直接返回
if (!nodeName) {
return encodeEntities(vnode);
}
// 處理組件
if (typeof nodeName==='function') {
isComponent = true;
if (opts.shallow && (inner || opts.renderRootComponent===false)) {
nodeName = getComponentName(nodeName);
}
else {
...
if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') {
// 處理無狀態函數式組件
...
}
else {
// 處理類組件
...
}
//遞歸處理下一層元素
return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
}
}
// 將 JSX 渲染到 HTML
let s = '', html;
if (attributes) {
let attrs = objectKeys(attributes);
//處理所有元素屬性
...
}
// 處理多行屬性
...
if (html) {
// 處理多行縮進
...
}
else {
// 遞歸處理子元素
...
}
...
return s;
}
Preact 的實現還是比較簡單明了的,我們繼續來看下 React 中涉及到服務端渲染相關的代碼,其主要涉及到 ReactDOMServer.js, ReactServerRendering.js, instantiateReactComponent.js, ReactCompositeComponent.js 以及 ReactReconciler.js 等幾個文件,其中前兩個文件算是專注于服務端渲染,而后三個文件則是用于定義 React 組件以及組件系統的組合與調和機制,其并不耦合于某個具體的平臺,也是主要的以犧牲性能來換取較好地抽象層次的實現類。首先我們來從應用的角度考慮下兩個可能影響服務端渲染性能的因素,一個是對于環境變量的設置。在 React 的源代碼中我們可以發現很多如下的調試語句:
if (process.env.NODE_ENV !== 'production') {
...
}
顯而易見如果我們沒有將環境變量設置為 production ,勢必會在運行時調用更多的調試代碼,拖慢整體性能。另一個有可能拖慢服務端渲染性能的因素是 React 在生成 HTML 后會對元素進行校驗和計算并且附加到元素屬性中:
<div data-reactroot="" data-reactid="1" data-react-checksum="-492408024">
...
</div>
上述代碼中的 data-react-checksum 就是計算而來的校驗和,該計算過程是會占用部分時間,不過影響甚微。筆者對于 renderToStringImpl 函數進行了斷點性能分析,主要是利用 console.time 記錄函數執行時間并且進行對比:
...
return transaction.perform(function () {
var componentInstance = instantiateReactComponent(element, true);
var reactDOMContainerInfo = ReactDOMContainerInfo();
console.time('transaction');
console.log('transaction 開始:' + Date.now());
var markup = ReactReconciler.mountComponent(componentInstance, transaction, null, reactDOMContainerInfo, emptyObject, 0 /* parentDebugID */
);
console.log('transaction 結束:' + Date.now());
console.timeEnd('transaction');
...
if (!makeStaticMarkup) {
console.time('markup');
markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
console.timeEnd('markup');
}
return markup;
...
// 運行結果為:
// transaction: 12.643ms
// markup: 0.249ms
從運行結果上可以看出,計算校驗和并未占用過多的時間比重,因此這也不會是拖慢服務端渲染性能的主因。實際上當我們調用 ReactDOMServer.renderToString 時,其會調用 ReactServerRendering.renderToStringImpl 這個內部實現,該函數的第二個參數 makeStaticMarkup 用來標識是否需要計算校驗和。換言之,如果我們使用的是 ReactDOMServer.renderToStaticMarkup ,其會將 makeStaticMarkup 設置為 true 并且不計算校驗和。完整的一次服務端渲染的對象與函數調用流程如下:
整個流程同樣是遞歸解析組件樹到 HTML 標記的過程,筆者同樣是以斷點計時的方式進行追蹤,有趣的一個細節是從 Transaction 開始到首次調用ReactReconciler 中 mountComponent 函數之間間隔 2ms,換言之,有大量的時間花費在了具體的解析之外,可能這種類型的抽象帶來的額外消耗會是 React 服務端渲染性能較差的原因之一吧。
來自:https://segmentfault.com/a/1190000008258779