標籤:

React 服務端渲染緩慢原因淺析

React 服務端渲染緩慢原因淺析從屬於筆者的Web 前端入門與工程實踐。

前幾日筆者在服務端渲染性能大亂斗:Vue, React, Preact, Rax, Marko 一文中比較了當前流行的數個前端框架服務端渲染的性能表現,下圖數值越高越好:

筆者看完這個數據對比之後不由好奇,緣何 React 服務端渲染的性能會如此之差;從設計理念的角度來看 React 本身專註於跨平台的界面庫,其保證較好抽象層次的同時勢必會付出一定的代價,並且 Facebook 在生產環境中並未大規模應用服務端渲染,也就未花費過多的精力來優化服務端渲染的性能。筆者也對比了下 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 : " "; 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 服務端渲染性能較差的原因之一吧。最後補上使用 node-profiler 對於 React 與 Preact 服務端渲染運行函數調用對比的兩張圖:

推薦閱讀:

VRay for SketchUp工業產品表現之煤油燈
VRay for SketchUp環境阻光(AO)的簡介與應用
##譯## The Comprehensive PBR Guide by Allegorithmic — Vol.2
支持導入任意貼圖的maya工具——材質管理器

TAG:React | 渲染 |