React 服務端渲染緩慢緣由淺析從屬於筆者的Web 前端入門與工程實踐。html
前幾日筆者在服務端渲染性能大亂鬥:Vue, React, Preact, Rax, Marko 一文中比較了當前流行的數個前端框架服務端渲染的性能表現,下圖數值越高越好:前端
筆者看完這個數據對比以後不禁好奇,緣何 React 服務端渲染的性能會如此之差;從設計理念的角度來看 React 自己專一於跨平臺的界面庫,其保證較好抽象層次的同時勢必會付出必定的代價,而且 Facebook 在生產環境中並未大規模應用服務端渲染,也就未花費過多的精力來優化服務端渲染的性能。筆者也對比了下 React 與 Preact 有關服務端渲染的實現代碼,確實高度的抽象須要額外的代碼邏輯與對象建立,React 自己並無冗餘的部分,只是單純地大量的毫秒級別額外對象操做的耗時的累加致使了最後性能表現的巨大差別。咱們首先看下 Preact 的renderToString
的函數實現,其緊耦合於 DOM 環境,以較低的抽象程度換取較少的代碼實現:node
/** 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 的源代碼中咱們能夠發現不少以下的調試語句:react
if (process.env.NODE_ENV !== 'production') { ... }
顯而易見若是咱們沒有將環境變量設置爲production
,勢必會在運行時調用更多的調試代碼,拖慢總體性能。另外一個有可能拖慢服務端渲染性能的因素是 React 在生成 HTML 後會對元素進行校驗和計算而且附加到元素屬性中:git
<div data-reactroot="" data-reactid="1" data-react-checksum="-492408024"> ... </div>
上述代碼中的data-react-checksum
就是計算而來的校驗和,該計算過程是會佔用部分時間,不過影響甚微。筆者對於renderToStringImpl
函數進行了斷點性能分析,主要是利用console.time
記錄函數執行時間而且進行對比:github
... 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 服務端渲染性能較差的緣由之一吧。框架