React同構與極致的性能優化

xvqs9642 6年前發布 | 18K 次閱讀 React 性能優化 移動開發

注: 本文為 第12屆D2前端技術論壇《打造高可靠與高性能的React同構解決方案》 分享內容,已經過數據脫敏處理。

前言

  • 隨著React的興起, 結合Node直出的性能優勢和React的組件化,React同構已然成為趨勢之一。享受技術福利的同時,直面技術挑戰,在復雜場景下,挑戰10倍以上極致的性能優化。

什么是同構?

  • 一套代碼既可以在服務端運行又可以在客戶端運行,這就是同構應用。簡而言之, 就是服務端直出和客戶端渲染的組合, 能夠充分結合兩者的優勢,并有效避免兩者的不足。

為什么同構?

  • 性能: 通過Node直出, 將傳統的三次串行http請求簡化成一次http請求,降低首屏渲染時間
  • SEO: 服務端渲染對搜索引擎的爬取有著天然的優勢,雖然阿里電商體系對SEO需求并不強,但隨著國際化的推進, 越來越多的國際業務加入阿里大家庭,很多的業務依賴Google等搜索引擎的流量導入,比如Lazada.
  • 兼容性: 部分展示類頁面能夠有效規避客戶端兼容性問題,比如白屏。

性能數據

性能是一個綜合性的問題, 不能簡單地斷言同構應用一定比非同構應用性能好,只能說合適的場景加上合理的運用,同構應用確實能帶來一定的性能提升, 先來看一個線上的案例。

通常來說,網絡狀況越差,同構的優勢越明顯,下圖是在不同網絡狀況下首屏渲染時間的一組對比

線上案例

  • 近兩年,無論是業界還是阿里內部都涌現了大量同構實踐, 業界比較有影響力的包括非死book, Quora, Medium, 推ter, Airbnb, Walmart、手Q以及QQ興趣部落等
  • 阿里內部也有大量的應用,僅列舉部分 beidou開發組 做過技術支持的項目

    • 阿里云 - 大數據地產
    • 釘釘 - 企業主頁
    • 釘釘 - 釘釘日志和審批模板市場
    • 菜鳥 - 物流大市場
    • 云零售 - 店掌柜
    • Lazada - PDP
    • 國際事業部 - AGLA
    • AILab - 行業解決方案
    • AILab - 智能硬件平臺
    • AILab - AliGenie開放平臺
    • AILab - AR官網
    • ICBU - ICBU店鋪
    • 業務平臺 - 門店評價
    • 國際UED - 數據運營
    • 國際UED - 知之
    • 國際UED - 探花
    • 國際UED - Nuke官網及過程管理
    • 國際UED - 會議記錄,實時翻譯
    • 國際UED - LBS數據地圖
    • 國際UED - 數探
    • 國際UED - 微策
    • 國際UED - shuttle
    • 國際UED - fie portal
    • ...

業界生態

  • react-server : React服務端渲染框架
  • next.js : 輕量級的同構框架
  • beidou : 阿里自己的同構框架,基于eggjs, 定位是企業級同構框架

除了開源框架,底層方面React16重構了SSR, react-router提供了更加友好的SSR支持等等, 從某種程度上來說,同構也是一種趨勢,至少是方向之一。

思考 與 實現

同構的出發點不是 “為了做同構,所以做了”, 而是回歸業務,去解決業務場景中SEO、首屏性能、用戶體驗 等問題,驅動我們去尋找可用的解決方案。在這樣的場景下,除了同構本身,我們還需要考慮的是:

  • 高性能的 Node Server
  • 可靠的 同構渲染服務
  • 可控的 運維成本
  • 可復用的 解決方案
  • ...

簡單歸納就是, 我們需要一個 企業級的同構渲染解決方案。

我們是怎么做的?

基于 eggjs 加入可拔插的同構能力

  • beidou-plugin-react
    作為原有MVC架構中, view 層的替換, 使用 React 組件作為視圖層模板, 可以直接渲染 React Component 并輸出給客戶端
  • beidou-plugin-webpack
    集成 Webpack 到框架中, 在開發階段, 提供代碼的編譯和打包服務
  • beidou-plugin-isomorphic
    服務端的 React 運行時: babel-register
    polyfill 注入: 環境變量, BOM等
    非js文件解析: css, images, fonts...
  • 服務端支持css modules
  • 自動路由 : 純靜態頁面無需編寫任何服務端代碼,像寫純前端頁面一樣簡單
  • ...

這里不再贅述具體如何實現,有興趣的讀者可以閱讀我們的開源同構框架 beidou -- https://github.com/alibaba/beidou

熱點問題

任何一種技術都有其適用場景和局限性, 同構也不例外,以下試舉一二,以做拋磚引玉.

  • 內存泄漏
  • 性能瓶頸
  • ...

內存泄漏不是同構應用所特有的,理論上所有服務端應用都可能內存泄漏,但同構應用是“高危群體”, 具體如何解決請參考本人的 《Node應用內存泄漏分析方法論與實戰》 , 接下來重點剖析下性能優化。

極致的性能優化

前面也提到了,同構應用并不一定就比非同構應用性能好,影響性能的因素實在太多了,再來看一組數據

上圖是基于Node v8.9.1 和 React@15.5.4, 開4個進程采集到的數據, X軸是最終生成頁面節點數量,Y軸紅色的線表示RT(包括渲染時間和網絡時間), 綠色的柱子表示QPS. 可以看出來:

  • 隨著頁面節點的增多渲染時間可能變得很長,QPS下降非常迅速。在頁面節點超過3000左右的時候,QPS接近個位數了,而且實際頁面中可能包含較復雜的邏輯以及不友好的寫法,情況可能會更糟。

順帶提一下, 筆者采樣了 淘寶首頁淘寶某詳情頁 以及 Lazada某詳情頁 ,頁面節點數分別是2620、2467和3701. 大部分情況下,頁面節點數低于1000, 比如 菜鳥物流市場 首頁看起來內容不少,其實節點數是775.

那針對3000節點以上的頁面,我們該怎么做呢?筆者總結了以下策略并重點闡述其中一兩點:

  • 采用編譯后的React版本: 根據Sasha Aickin的博客,React15在Node4、Node6、Node8下,采用編譯后的版本性能相比未編譯版本分別提升了2.36倍、3倍、3.85倍
  • 模塊拆分: 模塊拆分有利于并發渲染,目前ICBU店鋪裝修采用的就是這種方式
  • 模塊級別緩存: 頁面中某些模塊其實是很適合緩存的,比如Lazada詳情頁中節點數雖然高達3701, 但其實頁頭部分就占比55.5%,頁尾占比3.5%,而頁頭頁尾是常年不變的.
  • 組件級緩存: 最小粒度的緩存單位了,性能提升依賴于緩存的范圍和命中率,運用得當,可能帶來非常大的性能提升。參考 walmartlabs
  • 采用hsf代替http對外提供服務: hsf的網絡消耗遠低于http, 在店鋪同構實踐中,改用hsf, java端調用Node端的耗時縮短了一半.
  • 部分模塊客戶端渲染(對SEO無用的部分): 直接降低SSR部分的復雜度
  • 智能降級: 當流量暴增,接近或超過閾值時,會直接導致服務的RT快速上升。可以實時監測CPU和內存的使用率,超過一定的比例自動降級為客戶端渲染,降低服務端壓力,CPU和內存恢復常態時,自動切回服務端渲染。
  • 采用Node8: 同樣在店鋪實踐中,采用Node8相比Node6, 渲染時間從28ms降低到了18ms, 提升幅度為36%.
  • 采用最新版React16: 非死book官方數據 , 在Node8下,React16相比編譯后的react15仍有3.8倍提升,相比未編譯的React15更是有數量級的提升。

組件級緩存

如果說性能優化有"萬能"的招式,那一定是緩存, 從Nigix緩存到模塊級緩存到組件級緩存,其中最讓人興奮的就是組件級緩存,讓我們一起來看看如何實現

  • 攔截React的渲染邏輯,業界主要有三種實現方式

    • Fork一份React, 暴力加入緩存邏輯, 代表庫是 react-dom-stream , 雖然這個庫的人氣很高,但筆者還是反對這種實現方式的。
    • 通過require hook攔截instantiateReactComponent的載入并注入緩存邏輯,參考 react-ssr-optimization
    • 擴展ReactCompositeComponent的mountComponent方法,參考 electrode-react-ssr-cachin
  • 注入緩存邏輯, 代碼如下
const ReactCompositeComponent = require("react/lib/ReactCompositeComponent");

ReactCompositeComponent.Mixin._mountComponent = ReactCompositeComponent.Mixin.mountComponent;
ReactCompositeComponent.Mixin.mountComponent = function(rootID, transaction, context) {

  const hashKey = generateHashKey(this._currentElement.props);
  if (cacheStorage.hasEntry(hashKey)) {
    // 命中緩存則直接返回緩存結果
    return cacheStorage.getEntry(hashKey);
  } else {
    // 若未命中,則調用react的mountComponent渲染組件,并緩存結果
    const html = this._mountComponent(rootID, transaction, context);
    cacheStorage.addEntry(hashKey, html);
    return html;
  }
};
  • 設置最大緩存和緩存更新策略
lruCacheSettings: {
      max: 500,  // The maximum size of the cache
      maxAge: 1000 * 5 // The maximum age in milliseconds
  }

上述緩存邏輯是基于屬性的,能覆蓋大部分的應用場景,但有一個要求,屬性值必須可枚舉且可選項很少. 請看下面的場景。

淘寶某頁面上有大量的商品,而淘寶的商品又何止百萬,就算某個被緩存,下次被命中的可能性依然微乎其微。那如何解決這個問題?聰明的讀者可能已經看出來了,雖然每個商品最終渲染的結果千變萬化,但結構始終是一致的,因此結構是可以緩存的。

要實現結構的緩存,需要在上述邏輯上額外新增三步。

  • 生成中間結構:

    • 以組件 <Price>${price}</Price> 為例,將變量price以占位符 ${price} 代替 set(price, "${price}") , 再調用react原生的mountComponent方法則可以生成中間結構 <div>${price}</div
  • 緩存中間結構
  • 生成最終組件

以上就是組件級緩存的實現方式, 特別要提醒的是緩存是把雙刃劍,運用不當可能會引發內存泄漏以及數據的不一致。

React16 SSR

  • FB在9.26發布了React16正式版,之前萬眾期待的 SSR性能提升 沒有讓大家失望, 引用React核心開發Sasha Aickin的對比圖

筆者拿之前的應用升級到React16, 對比下3909節點,RT從295ms降到了51ms, QPS從9提升到了44, 提升非常明顯。

實戰

接下來通過一個例子,展示如何一步步地提升性能。

代碼倉庫 -- https://github.com/alibaba/beidou/

10倍以上性能提升

  • 首先構造一個非常復雜的頁面, 頁面節點數是3342, 對比之下, 淘寶首頁 首屏的頁面節點數是831, 異步充分加載之后(懶加載完成),整個頁面節點數為3049. 注: 淘寶頁面為動態頁面,每次采樣可能會有差異。

  • 初始平均渲染時間為 295.75ms (Node6.92, React15.6.2), 注: 圖中有 296.50ms , 317.25ms , 297.25ms , 295.75ms 四個平均值,是因為開啟了四個進程,采樣最后一個,下同。

  • 采用Node8.9.1(或更新版本)平均渲染時間為 207ms

  • 采用 production 模式平均渲染時間為 81.75ms

  • 部分內容客戶端渲染,平均渲染時間為 44.63ms

  • 部分內容組件級別cache,平均渲染時間為 22.65ms

  • 采用React16(或更新版本),平均渲染時間為 5.17ms

  • 結合React16和部分客戶端渲染,平均渲染時間為 2.68ms

至此,服務端渲染時間已經最初的 295.75ms 降低到了 2.68ms ,提升了超過100倍。

更多性能策略

其實除了上述應用的策略,還有其它的策略,比如

  • 采用 Async , 有數據稱性能提升30%, 筆者試了下,未見明顯提升。應該是經過了babel的編譯,最終沒有發揮出 Async 的優勢,這是因為 beidou框架 在服務端要支持 import 等ES6的寫法以及支持React的 JSX語法 。其實也非常簡單,直接縮小 babel 的編譯范圍,在 beidou框架 中是可以自己定義的。
  • 降低React組件的嵌套層級。試驗數據,同樣的頁面節點數,服務端渲染時間和組件的嵌套層級是線性正相關的。
  • 熱點緩存

...

萬變不離其宗

借用《功夫》中的一句經典臺詞 天下武功,無堅不破,唯快不破 ,同樣的,

隨著時間的推移,上面這些策略策略遲早會 被破 ,比如react16 ssr重構之后,之前的組件級別緩存邏輯不再有效。

另外,可能由于架構設計/技術選型根本就使不上勁,比如react16是今年9月26才正式發版,很多第三方組件還沒來得及升級,如果應用中有些組件強依賴于react15或者更早的版本,可能根本就沒法利用react16的性能優勢。

那么有沒有一種 萬能的辦法 ,能夠做到 唯快不破 呢?

答案是: 有的。 只有掌握了方法論,才能在不斷變化中,找到適合自己應用的性能優化策略。

 

 

來自:https://segmentfault.com/a/1190000012464033

 

 本文由用戶 xvqs9642 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!