騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開

VasSonic成長曆程


前言

2017.8.8 14時,SNG增值產品部Vas團隊研發的輕量級高性能Hybrid框架VasSonic經過了公司最終審覈,做爲騰訊開源組件分享給你們。從當初立項優化頁面加載速度,到不斷摸索、優化,再到整理代碼、文檔,最終在Github上開源,而且在24小時內獲取star數超過1600。咱們很是高興看到咱們的成果收到這麼多的關注,趁此機會,正好回顧一下VasSonic的成長曆程,也但願可以讓你們更瞭解VasSonic。javascript

項目背景

Web相信你們再熟悉不過了,它具備快速迭代發佈的自然優點,但也存在中一些讓人詬病的問題,好比加載速度慢,體驗差等。在此以前,手Q上不少頁面首屏打開速度居高不下,甚至有些耗時達到3s以上,這意味着用戶打開頁面必須通過3秒以後才能進行交互操做,體驗至關差,不少用戶忍受不了這個漫長的時間直接流失掉了。css

爲了提高用戶體驗和業務用戶留存率,咱們不少業務一開始經過Web開發,等頁面模型驗證符合預期後,再將H5頁面轉化成原生界面。咱們很快意識到這不是一種健康的可持續的開發模式,一方面存在重複人力浪費,另一方面原生商城除了速度快一點,要運營活動改版都很難。html

因此後來團隊改了切入方向,安排人力專心研究如何加快頁面打開速度,通過了一系列的摸爬滾打和優化探索,最終咱們研發出了VasSonic框架,讓H5頁面首屏達到秒開,給用戶一個更好的H5體驗。下面就和你們分享VasSonic框架的發展歷程。前端

業務形態

任何一個技術框架都是結合具體的業務形態來進行發展優化的,技術是爲了更好地服務業務,業務也會驅動技術的發展。在此首先介紹一下業務形態,咱們是來自手Q增值產品部門的VAS團隊,負責手機QQ上不少深受年輕人喜歡的個性化增值服務,好比氣泡、掛件、主題等等。手Q上大部分的業務仍是基於H5開發的,你們對手Q的業務形態可能有簡單的瞭解。好比下圖的遊戲分發中心、會員特權中心、個性化裝扮商城等。這部分商城的特色比較明顯,頁面的不少數據都是動態的,是由咱們的產品經理在後臺配置的。 業務java

這些都是很常見頁面,咱們一般將html/js/css等靜態資源放到CDN上,而後頁面加載後,再經過CGI去拉取最新的數據,進行拼接展現, 這樣子能夠利用到CDN的多地部署和就近接入等優點,同時提升了服務器的併發能力。這種傳統模式的加載流程以下所示: </br> 加載流程git

  1. 用戶點擊後,通過終端一系列初始化流程,好比進程啓動、Runtime初始化、建立WebView等等。
  2. 完成初始化後,WebView開始去CDN上面請求Html加載頁面。
  3. 頁面發起CGI請求對應的數據或者經過localStorage獲取數據,數據回來後再對DOM進行操做更新

能夠看出上述流程存在着幾個問題:github

  1. 從外網統計數據來看,用戶的終端耗時在1s以上,這意味着在這1s多的時間裏,網絡徹底是空閒在等待的,很是浪費;
  2. 頁面的資源和數據徹底依賴於網絡,特別是用戶在弱網絡場景下,頁面會出現很長時間的白屏,體驗很是差;
  3. 由於頁面的數據依賴於動態拉取,加載完頁面後,每每是看到一些模塊先轉菊花,再展現,體驗也是很差的。同時這裏涉及到較多數據更新,常常要更新DOM,性能上也有很多開銷。

因此針對以上幾個問題,咱們也對應作了不少優化和探索。web

問題

VasSonic的前世

優化終端

針對終端耗時1s以上的狀況,咱們對手Q WebView框架進行了重構:算法

  1. 啓動流程完全拆分,設計爲一個狀態機按序按需執行
  2. View相關拆分模塊化設計,儘量懶加載,IO異步化
  3. X5內核在手Q中的獨立進程中提早預加載
  4. 建立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,但還存在很大的優化空間,當咱們準備持續深刻優化時,咱們的業務形態發生了新的變化。

以前咱們頁面內容的數據主要是由產品經理要配置的,用戶看到的內容基本都是同樣的。而如今頁面爲了更好地爲用戶推薦喜歡的內容,咱們後臺引入機器學習和隨機算法來作智能個性化推薦。好比左邊新用戶推薦的是新貨精選,而右邊活躍用戶展現的是潮品推薦。另外還有部分的內容是隨機算法推薦的。這意味着不一樣用戶看到的內容是不一樣的,同一個用戶不一樣時間看到的內容也有可能不一樣。

新業務

因此爲了知足業務的需求,咱們只能實時拉取用戶數據並在服務端渲染後返回給客戶端,也就是動態直出的方案。

可是動態直出方案存在幾個比較明顯的問題:

  1. 服務端實時拉取數據渲染致使白屏時間長,由於服務器要先實時拉取我的數據,而後進行渲染直出,這個耗時不可控;
  2. 首屏沒法使用離線預推等緩存策略,由於每一個用戶看到的內容不同,咱們沒法經過靜態直出的方式那樣把Html所有發佈到CDN;

雖然動態直出方案下,頁面首屏沒法經過離線預推等方式進行加載優化,但前面優化積累的經驗給咱們提供了思路:要優化白屏問題,核心仍是得從提高資源加載速度方向入手。因此咱們重點在資源加載方面進行了深度優化。

並行加載

首先在加載流程方面,咱們發現這裏WebView訪問依然是串行的, WebView要等終端初始化完成以後,才發起請求。雖然終端耗時優化了很多,可是從外網的統計數據來看,終端初始化仍是存在幾百毫秒的耗時,而這段時間內網絡是在空等的。

串行

所以性能上不夠極致,咱們優化代碼,這兩個操做並行處理,流程改成:

並行

並行處理後速度有所改善,但咱們發如今某些場景下,終端初始化比較快,但數據沒有完成返回,這意味着內核在空等,而內核是支持邊加載邊渲染的,咱們在並行的同時,可否也利用內核的這個特性呢?

因而咱們加入了一箇中間層來橋接內核和數據,內部稱爲流式攔截:

橋接流

  1. 啓動子線程請求頁面主資源,子線程中不斷講網絡數據讀取到內存中,也就是網絡流(NetStream)和內存流(MemStream)之間的轉換;
  2. 當WebView初始化完成的時候,提供一箇中間層BridgeStream來鏈接WebView和數據流;
  3. 當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

cache-offline字段說明

字段 說明
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
default mode VasSonic mode

展望將來

開源只是故事的開始,咱們仍會持續對 VasSonic 作改進,包括更易用的接口、更好的性能、更高的可靠性,同時快速響應解決開源後的issue和PR。這些改進最終也會原封不動地在手Q內使用,這一切都是爲了更快的WebView加載速度。 </br>

Talk is cheap,read the fucking code. If you are interested in VasSonic, don't forget to STAR VasSonic. Thank you for reading ~

相關文章
相關標籤/搜索