歡迎大家前往騰訊雲技術社群,獲取更多騰訊海量技術實踐乾貨哦~
作者:王斌
背景
興趣部落專案自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並進行直出頁面的自動化測試,只有通過自動化測試才可以提交程式碼。
這個方案極大的保證了直出服務的穩定,自此方案上線以來,再無直出服務出現問題情況發生~
展望
應用型技術的難點不是在克服技術問題(因為大部問題都是有解決方案的),而是在於能夠不斷的結合自身的產品體驗,發現其中存在的體驗問題,不斷使用更好的技術方案去優化使用者的體驗,為整個產品發展添磚加瓦。
做為公司最大的同構直出服務實踐,在後續的方案中,我們會進一步著手優化使用者的使用體驗。比如使用伺服器快取等手段來進一步減少伺服器端的耗時,優化直出圖片的載入的體驗等等,同時會更多豐富的實戰經驗分享給大家。
相關閱讀
此文已由作者授權騰訊雲技術社群釋出,轉載請註明文章出處
原文連結:https://cloud.tencent.com/community/article/531340