億萬級訪問量下的前端同構直出實踐

騰訊雲加社群發表於2019-03-04

歡迎大家前往騰訊雲技術社群,獲取更多騰訊海量技術實踐乾貨哦~

作者:王斌 

背景

興趣部落專案自2014年至今,一直都是採用的是前端渲染的模式,這種模式就是頁面html是一個空殼,首屏的內容需要css和js都載入完成後,請求cgi獲得資料後再渲染給使用者。這種模式的好處是可以讓後端和前端的工作完全分離,給日常的開發和維護帶來很大的便利。

我們在現在的工作模式上,為了儘可能的減少首屏耗時,做了相當多的優化,包括使用離線包的機制來減少css和js的時間。

但是這些所有的優化,仍然是基於JS執行後,才可以向使用者交付首屏的,如果遇到android上執行JS速度很慢的機器,就會顯得耗時仍然特別長。

使用直出的頁面,html不再只是一個空殼,而是一個渲染良好的頁面,這樣使用者就可以不用等待JS載入和執行後看到內容,大大減少使用者的焦慮感。

在現有的工作模式下,使用同構直出的手段,不僅可以保留我們現有的開發模式,還可以減少很多工作量。試想,我們現在將現有的工作模式全部推翻使用普通直出,要面臨多少工作重建。

同構直出,前後端完全使用同一套程式碼,將前端的渲染邏輯移到伺服器端完成,將渲染後的結果再交給使用者,得益於React這套體系,我們將這樣的能力應用到了興趣部落專案中。

歷程

首先我們先在一個小頁面上進行全量嘗試,不斷解決,調整其間遇到的問題。

後面,待我們的架構成熟了之後,我們把這套體系運用到興趣部落的三大核心頁面之一的帖子詳情頁。

經過不斷的灰度、解決問題,最終帖子詳情頁的同構直出正式全量上線

其中的機器,幾乎全是v4虛擬機器。

效果

直觀感受下對比的效果,如下圖

左側為前端渲染,右側為同構直出

可以明顯看出,直出在android機型下帶來的優化效果是非常明顯的,同時從正常的測速資料上來看,直出的首屏耗時減少了50%,慢速使用者佔比減少了3個百分點

架構

前端程式碼的架構是傳統是react+redux架構體系,使用redux的架構可以讓我們的直出更可控

挑戰

記憶體問題

同構直出大部分情況下都要面臨此類問題,普通的前端頁面極少會考慮記憶體洩露的原因,然而在node端執行的程式碼都要考慮記憶體洩露的問題。

一次使用者訪問的管道中,res.end()呼叫完了,理論上管道產生的記憶體可以完全被回收,如果不可以被回收,那麼就會產生記憶體一直增長的問題。

我們都知道,掛到GC ROOT上的變數都無法回收,前端的程式碼太多不控的程式碼會導致記憶體洩露,我們需要一個通用的解決方案

原來的程式碼

let Main = require(MainEntry);
複製程式碼

雖然每個請求,每個使用者都會去require同構的Main元件,但是由於node端require是單例模式,所以每個使用者引用的Main都是同一個引用,每個請求對Main(Main的執行)內部產生的變數宣告,如果該變數連線到Main的引用鏈上的,當使用者請求結束的時候是無法釋放的,因為Main的引用是單例的,會node快取住,所以這些變數就無法回收,會產生嚴重的記憶體洩露問題。

上線時記憶體暴漲的問題

為了解決這個問題,可以對每個使用者請求,開闢一個新的Main例項,這樣當使用者請求結束了,Main的引用可以被順利回收,就不會產生記憶體洩露的問題

目前部落中使用的是vm的解決方案,為每個使用者請求建立了一個沙箱環境

if(! mainVmScriptCache[entry]){
    var code = fs.readFileSync(require.resolve(entry), `utf8`);

    mainVmScriptCache[entry] = new vm.Script(m.wrap(code), {
        filename: entry
    });
}

//var startVmTime = + new Date();
var module = {
    exports: {}
};

var exports = module.exports;

mainVmScriptCache[entry].runInThisContext()(exports, require, module, __filename, __dirname);

Main = exports.default;
複製程式碼

每個使用者請求過來,都會重新變編譯出一個Main, 這個Main引用不與其他請求共享,請求結束了,Main也會被回收,Main中產生的所有垃圾內容都會被一起回收

記憶體得到有效控制

關於效能問題,vm產生的效能會帶來CPU的使用耗時增加,大約20ms,但對記憶體控制是非常有效的。

關於這塊的優化,同構直出本來就是一個CPU密集型的任務,後續可以結合快取來將CPU密集型任務轉為記憶體密集任務

二次CGI

雖然解決這個問題的方案並不難,但重在我們能在詳情頁放量前能發現這個常常被忽略的問題。

通用的重構直出方案,到前端的程式碼會正常執行,這樣cgi會在前端再發一次,資料也會變成最新的。但是,實際上,伺服器端已經為該使用者發一次請求了,這樣就導致了一個使用者請求了兩次cgi。

這裡的方案通常可以劃為優化的角度去考慮。

在第一個小頁面上線的時候,我們並沒有太重視這個問題,但是詳情頁灰度上線的時候,我們逐漸認識到這不是一個優化問題,而是一個嚴重的架構問題。如果詳情頁直接上線,對後臺cgi帶來量的衝擊是非常大的,原本3億的日訪問量一下子變成6億的訪問量,這比30w變成60w對後臺的壓力要遠遠大的多。所以這個問題要在繼續放量前必須解決的問題。

解決的方案就是使用資料cache,將node端已請求的資料同時吐到前端去,這樣在前端請求的時候做一次攔截,檢查是否有資料快取,如果有的話就不再請求CGI, 這樣可以大大消除新增CGI的量。

但是遇到的問題,資料用url_引數做key儲存的時候,往往因為前後端不一致的引數導致快取無法匹配,比如前端使用了地理位置資訊引數,這個在伺服器端是無法換取到的。解決的方案就是將這些引數存到cookie裡,請求的時候node端可以用cookie快取的位置資訊資料。

(客戶端依賴引數使用cookie,快取命中率大大提高)

離線包

css資源、js資源使用離線包是比較想當然的事情,但是在部落轉為直出,接入離線包也遇到一些困難。

(js、css md5值很多)

使用者端的離線包版本是很多的,每個離線包版本對就沒的資源的md5又不一樣,直出的頁面引用的資源又該怎麼知道使用者本地離線包的md5是哪個呢?

我們使用瞭如下的解決方案:

在前端編譯離線包的時候,會把html內注入一段script,script作用是在當前頁面下種下一個代表版本號的資料(version),同時將此html命名成[version].html傳送到直出伺服器,那入由該離線包發出的直出請求都會帶上這個版本資訊,我們根據這個版本資訊將對就的[version].html做為本次直出要吐出頁面的模板,這樣到使用者端可以匹配到使用者離線包的資源。

首屏優先

首屏優先也是常常被大家忽略的體驗問題,大部分前端渲染的頁面都是如下的樣子

<html>
<head>
  <link href="main.css" />
</head>
<body>
  <div class="root"></div>


  <script src="lib.js"></script>
  <script src="render.js"></script>
</body>
</html>
複製程式碼

如果使用直出會變成這個樣子

<html>
<head>
  <link href="main.css" />
</head>
<body>
  <div class="root">
        <h1>title</h1>
        <div class="content">content</div>
  </div>

  <script src="lib.js"></script>
  <script src="render.js"></script>
</body>
</html>
複製程式碼

看起來也沒什麼問題,內容直接出現在.root裡了

但是我們經常會忽略一個體驗問題,這樣的頁面真的是會比非直出快麼?

答案是80%否定的!也不是大部分情況並不會比非直出快!甚至體驗上會比非直出更慢!

原因是要弄清楚瀏覽器首屏的出現時機,什麼時候瀏覽器會執行第一次paint ? 簡單來講,大部分情況下直出的dom元素並不會第一時間展示出來,而是等render.js執行完,才會展示首屏內容,如果render.js都載入並執行完,那麼我們直出的dom元素還有什麼意義,這又回到普通的前端渲染了,空殼架子又比原來還要多了,所以難免白屏時間會更長。

所以為了解決這個問題,我們要讓直出的dom節點可以第一時間展示出來,解決的方法也不難,可以使用懶載入,部落使用了更好async方案,第一時間展示首屏內容,第一時間載入JS,並且不阻塞DOM渲染,不阻塞首屏交付。

感謝x5核心同學weetli的指導

關於首屏渲染時間:

  • css會阻塞渲染(paint) (css沒有載入完成渲染沒有意義)
  • js會阻塞文件解析,不會阻塞渲染
  • 瀏覽器解析到script標籤時,如果js資源已經準備好了,會先執行js,再做渲染,如果沒有執行好會先渲染
  • 大部分線上的cdn資源都是有強快取的,或者有手Q離線包,瀏覽器解析到script標籤時js資源已經準備好,會先執行js,再做渲染

首屏渲染的時機涉及麼很多因素,很不可控,但是x5核心瀏覽器提供給了便利的控制方法來優化首屏時機

x5首屏渲染時機可以自己定義,新增meta標籤

<meta name="x5-pagetype" content="optpage">
複製程式碼

x5-pagetype有三種可選型別

  • default 自動首屏探測
  • optpage 首屏標籤
  • webapp 不做探測

首屏標籤為

<first-screen/>
複製程式碼

維穩

一個線上的後臺任務,最大的問題就是講穩定和容災,首先任務保證使用者的服務是穩定的,遇到一些突發問題時候,線上的頁面仍然可以穩定的提供服務。

相比傳統的直出,同構擁有更強的容災的能力,這也同構直出的魅力所在!因為在同構直出宕掉的時候,還有前端渲染頁面可以提供正常的服務,所以部落在部署頁面的存在兩種模式

現有的前端渲染路徑:buluo.qq.com/mobile/deta…

對應的直出頁面路徑: buluo.qq.com/mobile/v2/d…

比如這個直出頁面buluo.qq.com/mobile/v2/d… (模擬器開啟),去掉v2就是非直出頁面buluo.qq.com/mobile/deta… (模擬器開啟)

興趣部落直出專案在容災策略上提供了兩層容災策略

第一層 框架層 · 超時、出錯容錯

框架超時、出錯時候就會返回一個頁面原始的非直出html頁面,這樣到使用者端就可以走正常前端渲染。

第二層 運維層 · 服務當機容錯

這一層的容錯會放在服務機的前置層,簡單來講就是請求直出頁面出現5xx、4xx的錯誤,就會隱式的轉發路徑到不含v2的非直出頁面。

   location ^~ /mobile/v2/ {
        proxy_pass xxx;
        proxy_intercept_errors on;
        error_page 403 404 408 500 501 502 503 504 @buluo_static_page;
    }

    location @buluo_static_page {
        rewrite /v2/(.*)$ /mobile/$1 last;
    }
複製程式碼

即使整個直出服務完全掛掉,我們都不用擔心服務的可用性

自動化測試

另外一個層次,如何保證平時開發過程的穩定性,也是整個架構體系重要的一環,不要等到有問題的程式碼的發到線上才發現有問題。

部落在直出的開發維穩體系上,首次引入的了自動化測試+git hook的方案來保證提交的程式碼一定是不會出問題的。

其他同學提交的程式碼在push的時候會觸發本地prepush hook並進行直出頁面的自動化測試,只有通過自動化測試才可以提交程式碼。

這個方案極大的保證了直出服務的穩定,自此方案上線以來,再無直出服務出現問題情況發生~

展望

應用型技術的難點不是在克服技術問題(因為大部問題都是有解決方案的),而是在於能夠不斷的結合自身的產品體驗,發現其中存在的體驗問題,不斷使用更好的技術方案去優化使用者的體驗,為整個產品發展添磚加瓦。

做為公司最大的同構直出服務實踐,在後續的方案中,我們會進一步著手優化使用者的使用體驗。比如使用伺服器快取等手段來進一步減少伺服器端的耗時,優化直出圖片的載入的體驗等等,同時會更多豐富的實戰經驗分享給大家。

相關閱讀

WebRTC 前端實時通訊技術

鳥瞰前端 , 再論效能優化

全面瞭解 React License

此文已由作者授權騰訊雲技術社群釋出,轉載請註明文章出處

原文連結:https://cloud.tencent.com/community/article/531340

相關文章