天天看點

React 同構直出優化總結

作者:郭林爍 joeyguo

React 的實踐從去年在 PC QQ家校群開始,由于 PC 上的網絡及環境都相當好,是以在使用時可謂一帆風順,偶爾遇到點小磕絆,也能夠快速地填補磨平。而最近一段時間,我們将手Q的家校群重構成 React,除了原有架構上存在明顯問題的原因外,選擇React也是因為它确實有足夠的吸引力以及優勢,加之在PC家校群上的實踐經驗,斟酌下便開始了,到現在已有頁面線上上正常跑起。

由于移動端上的網絡及環境迥異,性能偏差。是以在移動端上用 React 時,遇到了不少的坑點,也花了一些力氣在上面。關于在移動端上的優化,可看我們團隊的另一篇文章的 React移動端web極緻優化

一提到優化,不得不提直出

關于這塊可以檢視 Node直出理論與實踐總結,這篇文章較詳細的分析直出的概念及一步步優化,也結合了 手Q家校群使用快速的資料直出方式來優化性能的總結與性能資料分析。

一提到 React,不得不提同構

同構基于服務端渲染,卻不止是服務端渲染。

服務端渲染的方案早在背景程式前後端包辦的時代上就有了,那時候使用JSP、PHP等動态語言将資料與頁面模版整合後輸出給浏覽器,一步到位

React 同構直出優化總結

這個時候,前端開發跟後端揉為一體,項目小的時候,前後端的開發和調試還真可以稱為一步到位。但當項目龐大起來的時候,無論是修改某個樣式要起一個龐大服務的尴尬,還是前後端糅合的地帶變得越來越難以維護,都很難過。

前後端分離後,服務端渲染的模式就開始被淡化了。這時候的服務端渲染比較尴尬,由于前後端的編碼語言不同,連頁面模闆都不能複用,隻能讓在前後端開發完成後,再将前端代碼改為給後端使用的頁面模闆,增大了工作量。最終也還是跟背景包辦殊途同歸。

Node 駕着祥雲騰空而來,谷歌 V8 引擎給力支援,衆前端拿着看家本領(JavaScript)開始涉足服務端,于是服務端渲染上又一步進階

React 同構直出優化總結

由于前後端時候的相同的語言,是以前後端在代碼的共用上達到了新的高度,頁面模版、node modules 都可以做成前後通用。同構的雛形,隻是共用的代碼還是有局限。

有了Node 後,前端便有了更多的想象空間。前端架構開始考慮相容服務端渲染,提供更友善的 API,前後端共用一套代碼的方案,讓服務端渲染越來越便捷。當然,不隻是 React 做了這件事,但 React 将這種思想推向高潮,同構的概念也開始廣為人傳。

React 同構直出優化總結

關于 React 網上已有大多教程,可以檢視阮老師的react-demos。關于 React 上的資料流管理方案,現在最為火熱的 Redux 應該是首選,具體可以檢視另一篇文章 React 資料流管理架構之Redux,此篇就不再贅述,下面講講 React 同構的理論與在手Q家校群上的具體實踐總結。

React 的虛拟 Dom 以對象樹的形式儲存在記憶體中,并存在前後端兩種展露原型的形式

React 同構直出優化總結

用戶端上,虛拟 Dom 通過 ReactDOM 的 Render 方法渲染到頁面中

服務端上,React 提供的另外兩個方法:ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 可将其渲染為 HTML 字元串。

完善的 Compponent 屬性及生命周期與用戶端的 render 時機是 React 同構的關鍵。

DOM 的一緻性

在前後端渲染相同的 Compponent,将輸出一緻的 Dom 結構。

不同的生命周期

在服務端上 Component 生命周期隻會到 componentWillMount,用戶端則是完整的。

用戶端 render 時機

同構時,服務端結合資料将 Component 渲染成完整的 HTML 字元串并将資料狀态傳回給用戶端,用戶端會判斷是否可以直接使用或需要重新挂載。

以上便是 React 在同構/服務端渲染的提供的基礎條件。在實際項目應用中,還需要考慮其他邊角問題,例如伺服器端沒有 window 對象,需要做不同處理等。下面将通過在手Q家校群上的具體實踐,分享一些同構的 Tips 及優化成果

手Q家校群使用 React + Redux + Webpack 的架構。

ReactDOMServer 提供 renderToString 和 renderToStaticMarkup 的方法,大多數情況使用 renderToString,這樣會為元件增加 checksum。

React 同構直出優化總結

React 在用戶端通過 checksum 判斷是否需要重新render。

相同時則不重新render,省略建立DOM和挂載DOM的過程,接着觸發 componentDidMount 等事件來處理服務端上的未盡事宜(事件綁定等),進而加快了互動時間;不同時,元件将用戶端上被重新挂載 render。

renderToStaticMarkup 則不會生成與 react 相關的data-*,也不存在 checksum,輸出的 html 如下

React 同構直出優化總結

在用戶端時元件會被重新挂載,用戶端重新挂載不生成 checknum( 也沒這個必要 ),是以該方法隻當服務端上所渲染的元件在用戶端不需要時才使用

React 同構直出優化總結

服務端上的産生的資料需要随着頁面一同傳回,用戶端使用該資料去 render,進而保持狀态一緻。服務端上使用 renderToString 而在用戶端上依然重新挂載元件的情況大多是因為在傳回 HTML 的時候沒有将服務端上的資料一同傳回,或者是傳回的資料格式不對導緻,開發時可以留意 chrome 上的提示如

React 同構直出優化總結

平台上的差異,服務端渲染隻會執行到 compnentWillMount 上,是以為了達到同構的目的,可以把拉取資料的邏輯寫到 React Class 的靜态方法上,一方面服務端上可以通過直接操作靜态方法來提前拉取資料再根據資料生成 HTML,另一方面用戶端可以在 componentDidMount 時去調用該靜态方法拉取資料。

這裡指影響元件 render 結果的資料,舉個例子,下面的元件由于在服務端與用戶端渲染上會因為元件上産生不同随機數的原因而導緻用戶端将重新渲染。

可以将 Math.random() 封裝至Component 的 props 中,在服務端上生成随機數并傳入到這個component中,進而保證随機數在用戶端和服務端一緻。如:

服務端上傳入randomNum

目前後端共用一套代碼的時候,像前端特有的 Window 對象,Ajax 請求 在後端是無法使用上的,後端需要去掉這些前端特有的對象邏輯或使用對應的後端方案,如後端可以使用 http.request 替代 Ajax 請求,是以需要進行平台區分,主要有以下幾種方式

1.代碼使用前後端通用的子產品,如 isomorphic-fetch

2.前後端通過webpack 配置 resolve.alias 對應不同的檔案,如

用戶端使用 /browser/request.js 來做 ajax 請求

服務端 webpack 上使用 /server/request.js 以 http.request 替代 ajax 請求

3.使用 webpack.DefinePlugin 在建構時添加一個平台區分的值,這種方式的在 webpack UglifyJsPlugin 編譯後,非目前平台( 不可達代碼 )的代碼将會被去掉,不會增加檔案大小。如

在服務端的 webpack 加上下面配置

在JS邏輯上做判斷

4.window 是浏覽器上特有的對象,是以也可以用來做平台區分

這是為了減少服務端的負擔,也是加快首屏展示時間,如在手Q家校群清單中存在 “我釋出的” 和 “全部” 兩個 tab,内容都為作業清單,此次實踐在服務端上隻處理首屏可視内容,即隻輸出 “我釋出的” 的完整HTML,另外一個tab的内容在用戶端上通過 react 的 dom diff 機制來動态挂載,無頁面重新整理的感覺。

React 同構直出優化總結

舉個例子,identity 預設為 UNKOWN,從背景拉取到資料後,更新其值,進而觸發 setButton 方法

同構時,由于服務端上已做了第一次資料拉取,是以上面代碼在用戶端上将由于 identity 已存在而導緻永不執行 setButton 方法,解決方式可在 componentDidMount 做相容處理

下圖為其中一種形式,先進行資料請求,再将請求到的資料 dispatch 一個 action,通過在reducer将資料進行 redux 的 state 化。還有其他方式,如直接 dispatch 一個 action,在action裡面去做資料請求,後續是一樣的,不過這樣就要求請求資料的子產品是 isomorphism 即前後端通用的。

React 同構直出優化總結

設計好 store state 是使用 redux 的關鍵,而在服務端上,合理的扁平化 state 能在其被序列化時,減少 CPU 消耗。

用戶端上,由于 react 中 setState 的異步機制,是以在同個component中觸發多個action,會出現一種情況是:第一個 action 對 state 的改變還沒來得及更新component時,第二個action便開始執行,即第二個 action 将使用到未更新的值。

而在同構中,如果第一個 action (如下的 fetchData)是在服務端執行了,第二個 action 在用戶端執行時将使用到的是第一個 action 對 state 改變後的值,即更新後的值。這時,同構需要做相容處理。

手Q家校群上使用了 immutable 來保證資料的不可變,提高資料對比速度,而在同構時需要注意兩點

1.服務端上,從 store 中拿到的 state 為immutable對象,需轉成 string 再同HTML傳回;

2.用戶端上,從服務端注入到HTML上的 state 資料,需要将其轉成 immutable對象,再放到 configureStore 中,如

實際上,如果是一個單獨的服務的話,可以使用babel提供的方式來讓node環境相容好 E6

但如果是以同一個直出伺服器,多個項目的直出代碼都放在這個服務上,那麼,還是建議使用 webpack 的方式去相容 ES6,減少 babel 對全局環境的影響。使用 webpack 的話,在項目完成後,可将 es6 代碼編譯成 es5 再放到真正的 server 上,這樣也可以減少動态編譯耗時。

使用webpack時,預設是将css檔案以 css in js 的方式打包起來,這種情況将增加服務端運作耗時,通過将 css 外鍊,或在webpack打包成獨立的css檔案後再inline進去,可以減少服務端的處理耗時及負荷。

上面提及使用webpack編譯後的代碼放到真正的server上去跑,在前端釋出前一般會進行代碼uglify,而後端實際上沒多大必要,在實際應用中發現,使用 UglifyJsPlugin 後運作服務端會報錯,需慎用。

當服務端代碼需要使用到 dirname 時,需在 webpack.config.js 配置 target 為 node,并在 node 中聲明filename和dirname為true,否則拿不到準确值,如在服務端代碼上添加 console.log(dirname); 和 console.log(__filenam );

在服務端使用的 webpack 上指定 target 為 node,如下

經 webpack 編譯後輸出如下代碼,可看出 dirname 和 filename 将正确輸出(注:需考慮生成的路徑是否能在不同系統上跑,如下圖是在window下,使用的是雙斜杠)。

React 同構直出優化總結

而不在webpack上配置時,dirname則為 / ,filename則為檔案名,這是不正确的。

React 同構直出優化總結

使用 webpack 将一個子產品編譯後将形成一個立即執行函數,函數中傳回對象。如果需要将編譯後的代碼也作為一個子產品供其他地方使用時,那麼需要重新将該子產品暴露出去( 如當業務上的直出代碼隻是作為直出伺服器的其中一個任務時,那麼需要将編譯後的代碼作為一個子產品 exports 出去,即在編譯後代碼前重新加上 module.exports =,進而直出服務将能夠使用到這個編譯後的子產品代碼 )。寫了一個 webpack 插件來自動添加 module.exports,比較簡單,有興趣的歡迎使用 webpack-add-module-expors,效果如下:

編譯前

React 同構直出優化總結

編譯後

React 同構直出優化總結

使用 webpack-add-module-expors編譯後将帶上module.exports

React 同構直出優化總結

當服務端上不想處理樣式子產品或一些浏覽器才需要的子產品(如前端上報)時,需要在服務端上将其忽略。嘗試 webpack 自帶的 webpack.IgnorePlugin 插件後出現一些奇奇怪怪的問題,重溫 如何開發一個 Webpack Loader ( 一 ) 時想起 webpack 在執行時會将原檔案經webpack loaders進行轉換,如 jsx 轉成 js等。是以想法是将在服務端上需要忽略的子產品,在loader前執行前就将其忽略。寫了個 ignored-loader,可以将需要忽略的子產品在 loader 執行前直接傳回空,是以後續就不再做其他處理,簡單但也滿足現有需求。

服務端上的耗時增加了,但整體上的首屏渲染完成時間大大減少。

服務端渲染方案将資料的拉取和模闆的渲染從用戶端移到了服務端,由于服務端的環境以及資料拉取存在優勢(詳見 Node直出理論與實踐總結),是以在相比下,這塊耗時大大減少,但确實存在,這兩塊耗時是服務端渲染相比于用戶端渲染在服務端上多出來。是以本次也做了耗時的資料統計,如下圖。

React 同構直出優化總結

從統計的資料上看,服務端上資料拉取的時間約 61.75 ms,服務端render耗時為16.32 ms,這兩塊時間的和為 78 ms,這耗時還是比較大。是以此次在同構耗時在計算上包含了服務端資料拉取與模闆渲染的時間。

服務端渲染時由于不需要等待 JS 加載和 資料請求(詳見 Node直出理論與實踐總結),在首屏展示時間耗時上将大大減少,此次在手Q家校群清單頁首屏渲染完成時間上,優化前平均耗時約1643.914 ms,而同構優化後平均耗時為 696.62 ms,有了 947ms 的優化,提升約 57.5% 的性能,秒開搓搓有餘!

React 同構直出優化總結
React 同構直出優化總結

1.優化前

React 同構直出優化總結

2.優化後(同構直出)

React 同構直出優化總結

可明顯看出同構直出後,白屏時間大大減少,可互動時間也得到了提前,産品體驗将變得更好。

服務端渲染的方式能夠很好的減少首屏展示時間,React 同構的方式讓前後端模闆、類庫、以及資料模型上共用,大大減少的服務端渲染的工作量。

由于在服務端上渲染模闆,render 時過多的調用棧增加了服務端負載,也增加了 CPU 的壓力,是以可以隻直出首屏可視區域,減少Component層級,減少調用棧,最後,做好容災方案,如真的服務端挂了( 雖然情況比較少 ),可以直接切換到普通的用戶端渲染方案,保證使用者體驗。

以上,便是近期在 React 同構上的實踐總結,如有不妥,懇請斧正,謝謝。

檢視更多文章 >>https://github.com/joeyguo/blog