摘要:經過一系列優化后,在平臺上,點擊到頁面首屏展示的耗時從平均多降低為,優化以上。而現在頁面為了更好地為用戶推薦喜歡的內容,我們后臺引入機器學習和隨機算法來做智能個性化推薦。另外還有部分的內容是隨機算法推薦的。
VasSonic成長歷程
2017.8.8 14時,SNG增值產品部Vas團隊研發的輕量級高性能Hybrid框架VasSonic通過了公司最終審核,作為騰訊開源組件分享給大家。從當初立項優化頁面加載速度,到不斷摸索、優化,再到整理代碼、文檔,最終在Github上開源,并且在24小時內獲取star數超過1600。我們非常高興看到我們的成果收到這么多的關注,趁此機會,正好回顧一下VasSonic的成長歷程,也希望能夠讓大家更了解VasSonic。
項目背景Web相信大家再熟悉不過了,它具有快速迭代發布的天然優勢,但也存在中一些讓人詬病的問題,比如加載速度慢,體驗差等。在此之前,手Q上很多頁面首屏打開速度居高不下,甚至有些耗時達到3s以上,這意味著用戶打開頁面必須經過3秒之后才能進行交互操作,體驗相當差,很多用戶忍受不了這個漫長的時間直接流失掉了。
為了提升用戶體驗和業務用戶留存率,我們很多業務一開始通過Web開發,等頁面模型驗證符合預期后,再將H5頁面轉化成原生界面。我們很快意識到這不是一種健康的可持續的開發模式,一方面存在重復人力浪費,另外一方面原生商城除了速度快一點,要運營活動改版都很難。
所以后來團隊改了切入方向,安排人力專心研究如何加快頁面打開速度,經過了一系列的摸爬滾打和優化探索,最終我們研發出了VasSonic框架,讓H5頁面首屏達到秒開,給用戶一個更好的H5體驗。下面就和大家分享VasSonic框架的發展歷程。
業務形態任何一個技術框架都是結合具體的業務形態來進行發展優化的,技術是為了更好地服務業務,業務也會驅動技術的發展。在此首先介紹一下業務形態,我們是來自手Q增值產品部門的VAS團隊,負責手機QQ上很多深受年輕人喜歡的個性化增值服務,比如氣泡、掛件、主題等等。手Q上大部分的業務還是基于H5開發的,大家對手Q的業務形態可能有簡單的了解。比如下圖的游戲分發中心、會員特權中心、個性化裝扮商城等。這部分商城的特點比較明顯,頁面的很多數據都是動態的,是由我們的產品經理在后臺配置的。
這些都是很常見頁面,我們通常將html/js/css等靜態資源放到CDN上,然后頁面加載后,再通過CGI去拉取最新的數據,進行拼接展示, 這樣子可以利用到CDN的多地部署和就近接入等優勢,同時提高了服務器的并發能力。這種傳統模式的加載流程如下所示:
用戶點擊后,經過終端一系列初始化流程,比如進程啟動、Runtime初始化、創建WebView等等。
完成初始化后,WebView開始去CDN上面請求Html加載頁面。
頁面發起CGI請求對應的數據或者通過localStorage獲取數據,數據回來后再對DOM進行操作更新
可以看出上述流程存在著幾個問題:
從外網統計數據來看,用戶的終端耗時在1s以上,這意味著在這1s多的時間里,網絡完全是空閑在等待的,非常浪費;
頁面的資源和數據完全依賴于網絡,特別是用戶在弱網絡場景下,頁面會出現很長時間的白屏,體驗非常差;
因為頁面的數據依賴于動態拉取,加載完頁面后,往往是看到一些模塊先轉菊花,再展示,體驗也是不好的。同時這里涉及到較多數據更新,經常要更新DOM,性能上也有不少開銷。
所以針對以上幾個問題,我們也對應做了很多優化和探索。
VasSonic的前世 優化終端針對終端耗時1s以上的情況,我們對手Q WebView框架進行了重構:
啟動流程徹底拆分,設計為一個狀態機按序按需執行
View相關拆分模塊化設計,盡可能懶加載,IO異步化
X5內核在手Q中的獨立進程中提前預加載
創建WebView對象復用池
關于第四點,我們想分享一些Android平臺上的細節,由于Android系統的生態原因,導致用戶的系統版本和系統Webkit內核處于極其分裂狀態,所以我們公司在手Q和微信統一使用X5內核。相對系統WebView來說,首次啟動X5內核時,創建WebView比較耗時,因此我們盡量想復用WebView,但是WebView卻是與Activity Context綁定。銷毀復用的時候,需要釋放Activity的Context,否則會內存泄露。針對這種情況,有沒有一種兩全其美的辦法呢?
計算機有一句經典的名言:計算機領域任何一個問題都可以通過引入中間層來解決。于是我們通過包裝的方式,實現了一個Context的殼,真正的實現體包裝在里面,邏輯調用真正調用到對應的實現體的函數。 經過實驗發現,Android系統本身提供了這么一個MutableContextWrapper,作為Context的一個中間層。
我們會將Activity context包在MutableContextWrapper里面,destory的時候,會將WebView的Context設置為Application的Context,從而釋放Activity Context。
類似如下:
//precreate WebView MutableContextWrapper contextWrapper = new MutableContextWrapper(BaseApplicationImpl.sApplication); mPool[0] = new WebView(contextWrapper); //reset WebView ct =(MutableContextWrapper)webview.getContext(); ct.setBaseContext(getApplication()); //reuse WebView ((MutableContextWrapper)webview.getContext()).setBaseContext(activityContext);靜態直出
“直出”這個概念對前端同學來說,并不陌生。為了優化首屏體驗,大部分主流的頁面都會在服務器端拉取首屏數據后通過NodeJs進行渲染,然后生成一個包含了首屏數據的Html文件,這樣子展示首屏的時候,就可以解決內容轉菊花的問題了。
當然這種頁面“直出”的方式也會帶來一個問題,服務器需要拉取首屏數據,意味著服務端處理耗時增加。
不過因為現在Html都會發布到CDN上,WebView直接從CDN上面獲取,這塊耗時沒有對用戶造成影響。
手Q里面有一套自動化的構建系統Vnues,當產品經理修改數據發布后,可以一鍵啟動構建任務,Vnues系統就會自動同步最新的代碼和數據,然后生成新的含首屏Html,并發布到CDN上面去。
頁面發布到CDN上面去后,那么WebView需要發起網絡請求去拉取。當用戶在弱網絡或者網速比較差的環境下,這個加載時間會很長。于是我們通過離線預推的方式,把頁面的資源提前拉取到本地,當用戶加載資源的時候,相當于從本地加載,即使沒有網絡,也能展示首屏頁面。這個也就是大家熟悉的離線包。
手Q使用7Z生成離線包, 同時離線包服務器將新的離線包跟業務對應的歷史離線包進行BsDiff做二進制差分,生成增量包,進一步降低下載離線包時的帶寬成本,下載所消耗的流量從一個完整的離線包(253KB)降低為一個增量包(3KB)。
經過一系列優化后,在Android平臺上,點擊到頁面首屏展示的耗時從平均3s多降低為1.8s,優化40% 以上。
VasSonic的誕生雖然通過靜態直出和離線預推等方式優化后,速度已經達到1.8s,但還存在很大的優化空間,當我們準備持續深入優化時,我們的業務形態發生了新的變化。
之前我們頁面內容的數據主要是由產品經理要配置的,用戶看到的內容基本都是一樣的。而現在頁面為了更好地為用戶推薦喜歡的內容,我們后臺引入機器學習和隨機算法來做智能個性化推薦。比如左邊新用戶推薦的是新貨精選,而右邊活躍用戶展示的是潮品推薦。另外還有部分的內容是隨機算法推薦的。這意味著不同用戶看到的內容是不同的,同一個用戶不同時間看到的內容也有可能不同。
所以為了滿足業務的需求,我們只能實時拉取用戶數據并在服務端渲染后返回給客戶端,也就是動態直出的方案。
但是動態直出方案存在幾個比較明顯的問題:
服務端實時拉取數據渲染導致白屏時間長,因為服務器要先實時拉取個人數據,然后進行渲染直出,這個耗時不可控;
首屏無法使用離線預推等緩存策略,因為每個用戶看到的內容不一樣,我們無法通過靜態直出的方式那樣把Html全部發布到CDN;
雖然動態直出方案下,頁面首屏無法通過離線預推等方式進行加載優化,但前面優化積累的經驗給我們提供了思路:要優化白屏問題,核心還是得從提升資源加載速度方向入手。所以我們重點在資源加載方面進行了深度優化。
并行加載首先在加載流程方面,我們發現這里WebView訪問依然是串行的, WebView要等終端初始化完成之后,才發起請求。雖然終端耗時優化了不少,但是從外網的統計數據來看,終端初始化還是存在幾百毫秒的耗時,而這段時間內網絡是在空等的。
因此性能上不夠極致,我們優化代碼,這兩個操作并行處理,流程改為:
并行處理后速度有所改善,但我們發現在某些場景下,終端初始化比較快,但數據沒有完成返回,這意味著內核在空等,而內核是支持邊加載邊渲染的,我們在并行的同時,能否也利用內核的這個特性呢?
于是我們加入了一個中間層來橋接內核和數據,內部稱為流式攔截:
啟動子線程請求頁面主資源,子線程中不斷講網絡數據讀取到內存中,也就是網絡流(NetStream)和內存流(MemStream)之間的轉換;
當WebView初始化完成的時候,提供一個中間層BridgeStream來連接WebView和數據流;
當WebView讀取數據的時候,中間層BridgeStream會先把內存的數據讀取返回后,再繼續讀取網絡的數據。
通過這種橋接流的方式,整個內核無需等待,繼續做到邊加載邊解析。這種并行的方式讓首屏的速度優化15%以上,進一步提升了頁面加載速度。
動態緩存通過并行加載,我們極大地提升了WebView請求的速度,但是在弱網絡場景下白屏時間還是非常長,用戶體驗非常糟糕。于是我們在思考,是否能夠將用戶的已經加載的頁面內容緩存下來,等用戶下此點擊頁面的時候,我們先加載展示頁面緩存,第一時間讓用戶看到內容,然后同時去請求新的頁面數據,等新的頁面數據拉取下來之后,我們再重新加載一遍即可。
保存頁面內容這個工作很簡單,因為現在我們資源讀取都是通過中間層BridgeStream來管理的,只需要將整個讀取的內容緩存下來即可。
于是我們就按動態緩存這種方案去實現了,但很快就發現了問題。用戶打開頁面之后,先是看到歷史頁面,等用戶準備去操作的時候,突然頁面白閃一下,重新加載了一遍,這種體驗非常差,特別在一些低端機器上,這個白閃的過程太明顯,非常影響體驗,這是用戶和產品經理都不能接受的。于是我們在思考,能否只做局部的刷新,僅刷新變化的元素呢?
通過分析,我們發現同一個用戶的頁面,大部分數據都是不變的,經常變化的只有少量數據,于是我們提出了模板(template)和數據塊(data)的概念:頁面中經常變化的數據我們稱為數據塊,除了數據塊之外的數據稱為模板。
頁面分離我們將整個頁面html通過VasSonic標簽進行劃分,包裹在標簽中的內容為data,標簽外的內容為模版。
首先我們對Html內容進行了擴展,通過代碼注釋的方式,增加了“sonicdiff-xxx”來標注一個數據塊的開始與結束。
而模板就是將數據塊摳掉之后的Html,然后通過{albums}來表示這個是一個數據塊占位。
數據就是JSON格式,直接Key-Value。
當然,為了完美地兼容Html,我們對協議頭部進行了擴展,比如增加accept-diff來標注是否支持增量更新、template-tag來標注模板的md5是多少等。OK,有了上面這個規則或者公式后,我們就可以實現增量更新了。
VasSonic為了支持區分客戶端是否支持增量更新等能力,對頭部字段進行了擴展
字段 | 說明 | 請求頭(Y/N) | 響應頭(Y/N) |
---|---|---|---|
accept-diff | 表示終端是否支持VasSonic模式,true為支持,否則不支持 | Y | N |
If-none-match | 本地緩存的etag,給服務端判斷是否命中304 | Y | N |
etag | 頁面內容的唯一標識(哈希值) | N | Y |
template-tag | 模版唯一標識(哈希值),客戶端使用本地校驗 或 服務端使用判斷是模板有變更 | Y | Y |
template-change | 標記模版是否變更,客戶端使用 | N | Y |
cache-offline | 客戶端端使用,根據不同類型進行不同行為 | N | Y |
字段 | 說明 |
---|---|
true | 緩存到磁盤并展示返回內容 |
false | 展示返回內容,無需緩存到磁盤 |
store | 緩存到磁盤,如果已經加載緩存,則下次加載,否則展示返回內容 |
http | 容災字段,如果http表示終端六個小時之內不會采用sonic請求該URL |
VasSonic根據本地是否有緩存以及本地緩存數據跟服務器數據的差異情況分為以下四種模式。
模式 | 說明 | 條件 |
---|---|---|
首次加載 | 本地沒有緩存,即第一次加載頁面 | etag為空值或template_tag為空值 |
完全緩存 | 本地有緩存,且緩存內容跟服務器內容完全一樣 | etag一致 |
數據更新 | 本地有緩存,本地模版內容跟服務器模版內容一樣,但數據塊有變化 | etag不一致 且 template_tag一致 |
模版更新 | 本地有緩存,緩存的模版內容跟服務器的模版內容不一樣 | etag不一致 且 template_tag不一致 |
我們會在請求頭部帶上支持accept-diff為true和sdk版本號等標識著首次加載的信息。當請求返回后,VasSonic會在延遲幾秒后(避免激烈IO競爭)將頁面抽離成模板和數據并保存到本地。此時終端緩存目錄下,該頁面將對應三個緩存文件xxx.html、xxx.template、xxx.data,其中xxx是該頁面的唯一標識(即sonicSessionId)。
對于頁面非首次加載場景,VasSonic優先加載本地緩存, 同時我們會在請求頭部帶上當前緩存和模板的md5,后臺進行模板md5對比之后,分為以下幾種情況:
非首次加載之完全緩存本地有緩存,且緩存內容跟服務器內容完全一樣.
非首次加載之增量數據如果模板發現沒有變化,那么會在響應頭部返回template-change=false,同時響應包體返回的數據不再是完整的html,而是一段JSON數據,及全部的數據塊。我們現在需要跟本地數據進行差分,找出真正的增量數據,如上圖中,后臺返回了N個數據,實際上僅有一個數據是有變化的,那么我們僅需要將這個變化的數據提交到頁面即可。一般場景下,這個差異的數據比全部數據要小很多。如果頁面拆分數據得更細,那么頁面的變動就更小,這個取決于前端同學對數據塊的細化程度。
獲得變化數據塊(diff_data)后,客戶端只需要通知頁面頁面設置的回調接口(getDiffDataCallback)進行界面元素更新即可。這里javascript的通信方式也可以自由定義(可以使用webview標準的javascript通信方式,也可以使用偽協議的方式),只要頁面跟終端協商一致就可以。
對于數據更新這種場景,終端還會將新的數據和模板拼接成為新的頁面,保持緩存最新。當終端初始化比較慢的時候,WebView去加載緩存的時候,這個頁面可能已經是最新的了,連數據刷新都不需要。
非首次加載之模板更新與數據更新模式不一樣,由于業務需求,頁面的模板會發生更改。當終端在獲取到新的模板和數據后,本地在子線程中進行合并,生成一個新的緩存,然后回調通知終端,刷新WebView來加載新的緩存。
我們來看一下最終的流程圖,跟動態緩存對比,有不少細節優化:
我們從第2步開始,SonicSession首先會去讀取緩存。會拋個消息通知WebView讀取緩存,如果Webview已經準備好,則直接加載緩存,如果沒有,則緩存先放在內存里面。同時SonicSession也會帶上模板等信息到后臺拉取新的內容,后臺經過Sonic-Diff之后,會返回新的數據。SonicSession拿到新的數據后,首先會跟本地數據進行Diff,如果發現WebView已經加載緩存,則直接提交增量數據給頁面。否則繼續拼接最新的頁面,替換掉內存里面的緩存,同時保存到本地。這個時候WebView如果Ready,則直接進行第5步load最新的內容即可。
效果統計這個是我們外網的統計數據。在數據更新模式下,首屏的耗時在1s左右,相比普通的動態直出,優化了50%以上。模板更新這個會比首次高,是因為加載了兩次頁面,不過從模式占比上來看,我們大部分頁面都是數據更新。針對模板更新這種耗時比較高的情況,前面優化積累的經驗給我們提供了思路,核心還是從提前獲取資源方向入手,因此我們優先考慮如何預加載模板更新。
預加載實際上整個SonicSession在沒有WebView的情況下,也是可以獨立完成所有邏輯的,當用戶點擊頁面的時候,我們在將WebView和SonicSession綁定起來即可。于是我們支持了兩種預加載的模式,一種是通過后臺push的方式,來提前獲取數據。還有一種就是JSAPI,頁面可以調用JSAPI來預加載用戶可能操作的下一個頁面。通過這兩種方式,我們可以把需要的增量更新數據提前拉取回來
Pic 1: 沒有使用VasSonic | Pic 2: 使用VasSonic |
---|---|
開源只是故事的開始,我們仍會持續對 VasSonic 做改進,包括更易用的接口、更好的性能、更高的可靠性,同時快速響應解決開源后的issue和PR。這些改進最終也會原封不動地在手Q內使用,這一切都是為了更快的WebView加載速度。
Talk is cheap,read the fucking code. If you are interested in VasSonic, don"t forget to STAR VasSonic.
Thank you for reading ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/51132.html
2017-08-18 前端日報 精選 [譯] 關于 React Router 4 的一切騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開用簡單的方法學習ECMAScript 6【譯】一個小時搭建一個全棧Web應用框架你應該知道的Debug技巧Understanding V8’s Bytecode – DailyJS – MediumAnnouncing TypeScript 2.5 RC 中文...
閱讀 2941·2021-10-14 09:42
閱讀 3706·2021-08-11 11:19
閱讀 3552·2019-08-30 13:57
閱讀 3132·2019-08-30 13:49
閱讀 1545·2019-08-29 18:38
閱讀 905·2019-08-29 13:16
閱讀 1861·2019-08-26 13:25
閱讀 3235·2019-08-26 13:24