陳志興筐咧,騰訊 SNG 增值產(chǎn)品部高級工程師,主要負責(zé)手 Q 個性化業(yè)務(wù)地沮、手 Q WebView 等項目嗜浮。喜歡閱讀優(yōu)秀的開源項目,聽聽音樂摩疑,偶爾也會打打競技類游戲危融。
本文根據(jù)作者在 2017GMTC 全球移動技術(shù)大會的上分享的 ppt 整理:
http://ppt.geekbang.org/slide/download/862/593b7cecd6ce2.pdf/19
特別感謝盧景倫(騰訊 SNG 增值產(chǎn)品部高級工程師)將 ppt 精華匯總成文,方便大家閱讀學(xué)習(xí)雷袋。
2017 年 8 月 8 日吉殃,SNG 增值產(chǎn)品部 Vas 團隊研發(fā)的輕量級高性能 Hybrid 框架 VasSonic 通過了公司最終審核,作為騰訊開源組件分享給大家楷怒。從當(dāng)初立項優(yōu)化頁面加載速度蛋勺,到不斷摸索、優(yōu)化鸠删,再到整理代碼抱完、文檔,最終在 Github 上開源刃泡,并且在 24 小時內(nèi)獲取 star 數(shù)超過 1600巧娱。我們非常高興看到我們的成果收到這么多的關(guān)注,趁此機會烘贴,正好回顧一下 VasSonic 的成長歷程禁添,也希望能夠讓大家更了解 VasSonic。
VasSonic GitHub 鏈接:
https://github.com/Tencent/VasSonic
Web 相信大家再熟悉不過了桨踪,它具有快速迭代發(fā)布的天然優(yōu)勢老翘,但也存在中一些讓人詬病的問題,比如加載速度慢,體驗差等铺峭。在此之前墓怀,手 Q 上很多頁面首屏打開速度居高不下,甚至有些耗時達到 3s 以上逛薇,這意味著用戶打開頁面必須經(jīng)過 3 秒之后才能進行交互操作捺疼,體驗相當(dāng)差,很多用戶忍受不了這個漫長的時間直接流失掉了永罚。
為了提升用戶體驗和業(yè)務(wù)用戶留存率,我們很多業(yè)務(wù)一開始通過 Web 開發(fā)卧秘,等頁面模型驗證符合預(yù)期后呢袱,再將 H5 頁面轉(zhuǎn)化成原生界面。我們很快意識到這不是一種健康的可持續(xù)的開發(fā)模式翅敌,一方面存在重復(fù)人力浪費羞福,另外一方面原生商城除了速度快一點,要運營活動改版都很難蚯涮。
所以后來團隊改了切入方向治专,安排人力專心研究如何加快頁面打開速度,經(jīng)過了一系列的摸爬滾打和優(yōu)化探索遭顶,最終我們研發(fā)出了 VasSonic 框架张峰,讓 H5 頁面首屏達到秒開,給用戶一個更好的 H5 體驗棒旗。下面就和大家分享 VasSonic 框架的發(fā)展歷程喘批。
任何一個技術(shù)框架都是結(jié)合具體的業(yè)務(wù)形態(tài)來進行發(fā)展優(yōu)化的,技術(shù)是為了更好地服務(wù)業(yè)務(wù)铣揉,業(yè)務(wù)也會驅(qū)動技術(shù)的發(fā)展饶深。在此首先介紹一下業(yè)務(wù)形態(tài),我們是來自手 Q 增值產(chǎn)品部門的 VAS 團隊逛拱,負責(zé)手機 QQ 上很多深受年輕人喜歡的個性化增值服務(wù)敌厘,比如氣泡、掛件朽合、主題等等俱两。手 Q 上大部分的業(yè)務(wù)還是基于 H5 開發(fā)的,大家對手 Q 的業(yè)務(wù)形態(tài)可能有簡單的了解旁舰。比如下圖的游戲分發(fā)中心锋华、會員特權(quán)中心、個性化裝扮商城等箭窜。這部分商城的特點比較明顯毯焕,頁面的很多數(shù)據(jù)都是動態(tài)的,是由我們的產(chǎn)品經(jīng)理在后臺配置的。
這些都是很常見頁面纳猫,我們通常將 html/js/css 等靜態(tài)資源放到 CDN 上婆咸,然后頁面加載后,再通過 CGI 去拉取最新的數(shù)據(jù)芜辕,進行拼接展示尚骄, 這樣子可以利用到 CDN 的多地部署和就近接入等優(yōu)勢,同時提高了服務(wù)器的并發(fā)能力侵续。這種傳統(tǒng)模式的加載流程如下所示:
用戶點擊后倔丈,經(jīng)過終端一系列初始化流程,比如進程啟動状蜗、Runtime 初始化需五、創(chuàng)建 WebView 等等。
完成初始化后轧坎,WebView 開始去 CDN 上面請求 Html 加載頁面宏邮。
頁面發(fā)起 CGI 請求對應(yīng)的數(shù)據(jù)或者通過 localStorage 獲取數(shù)據(jù),數(shù)據(jù)回來后再對 DOM 進行操作更新缸血。
可以看出上述流程存在著幾個問題:
從外網(wǎng)統(tǒng)計數(shù)據(jù)來看蜜氨,用戶的終端耗時在 1s 以上,這意味著在這 1s 多的時間里捎泻,網(wǎng)絡(luò)完全是空閑在等待的飒炎,非常浪費;
頁面的資源和數(shù)據(jù)完全依賴于網(wǎng)絡(luò)族扰,特別是用戶在弱網(wǎng)絡(luò)場景下厌丑,頁面會出現(xiàn)很長時間的白屏,體驗非常差渔呵;
因為頁面的數(shù)據(jù)依賴于動態(tài)拉取怒竿,加載完頁面后,往往是看到一些模塊先轉(zhuǎn)菊花扩氢,再展示耕驰,體驗也是不好的。同時這里涉及到較多數(shù)據(jù)更新录豺,經(jīng)常要更新 DOM朦肘,性能上也有不少開銷。
所以針對以上幾個問題双饥,我們也對應(yīng)做了很多優(yōu)化和探索媒抠。
?
VasSonic 的前世
優(yōu)化終端
針對終端耗時 1s 以上的情況,我們對手 Q WebView 框架進行了重構(gòu):
啟動流程徹底拆分咏花,設(shè)計為一個狀態(tài)機按序按需執(zhí)行
View 相關(guān)拆分模塊化設(shè)計趴生,盡可能懶加載阀趴,IO 異步化
X5 內(nèi)核在手 Q 中的獨立進程中提前預(yù)加載
創(chuàng)建 WebView 對象復(fù)用池
關(guān)于第四點,我們想分享一些 Android 平臺上的細節(jié)苍匆,由于 Android 系統(tǒng)的生態(tài)原因刘急,導(dǎo)致用戶的系統(tǒng)版本和系統(tǒng) Webkit 內(nèi)核處于極其分裂狀態(tài),所以我們公司在手 Q 和微信統(tǒng)一使用 X5 內(nèi)核浸踩。相對系統(tǒng) WebView 來說叔汁,首次啟動 X5 內(nèi)核時,創(chuàng)建 WebView 比較耗時检碗,因此我們盡量想復(fù)用 WebView据块,但是 WebView 卻是與 Activity Context 綁定。銷毀復(fù)用的時候折剃,需要釋放 Activity 的 Context瑰钮,否則會內(nèi)存泄露。針對這種情況微驶,有沒有一種兩全其美的辦法呢?
計算機有一句經(jīng)典的名言:
計算機領(lǐng)域任何一個問題都可以通過引入中間層來解決开睡。
于是我們通過包裝的方式因苹,實現(xiàn)了一個 Context 的殼,真正的實現(xiàn)體包裝在里面在刺,邏輯調(diào)用真正調(diào)用到對應(yīng)的實現(xiàn)體的函數(shù)耻台。 經(jīng)過實驗發(fā)現(xiàn)粹排,Android 系統(tǒng)本身提供了這么一個MutableContextWrapper
,作為 Context 的一個中間層款筑。
我們會將 Activity context 包在 MutableContextWrapper 里面,destory 的時候腾么,會將 WebView 的 Context 設(shè)置為 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);
“直出”這個概念對前端同學(xué)來說解虱,并不陌生攘须。為了優(yōu)化首屏體驗,大部分主流的頁面都會在服務(wù)器端拉取首屏數(shù)據(jù)后通過 NodeJs 進行渲染殴泰,然后生成一個包含了首屏數(shù)據(jù)的 Html 文件于宙,這樣子展示首屏的時候,就可以解決內(nèi)容轉(zhuǎn)菊花的問題了悍汛。
當(dāng)然這種頁面“直出”的方式也會帶來一個問題捞魁,服務(wù)器需要拉取首屏數(shù)據(jù),意味著服務(wù)端處理耗時增加离咐。
不過因為現(xiàn)在 Html 都會發(fā)布到 CDN 上谱俭,WebView 直接從 CDN 上面獲取,這塊耗時沒有對用戶造成影響。
手 Q 里面有一套自動化的構(gòu)建系統(tǒng) Vnues旺上,當(dāng)產(chǎn)品經(jīng)理修改數(shù)據(jù)發(fā)布后瓶蚂,可以一鍵啟動構(gòu)建任務(wù),Vnues 系統(tǒng)就會自動同步最新的代碼和數(shù)據(jù)宣吱,然后生成新的含首屏 Html窃这,并發(fā)布到 CDN 上面去。
?
離線預(yù)推
頁面發(fā)布到 CDN 上面去后征候,那么 WebView 需要發(fā)起網(wǎng)絡(luò)請求去拉取杭攻。當(dāng)用戶在弱網(wǎng)絡(luò)或者網(wǎng)速比較差的環(huán)境下,這個加載時間會很長疤坝。于是我們通過離線預(yù)推的方式兆解,把頁面的資源提前拉取到本地,當(dāng)用戶加載資源的時候跑揉,相當(dāng)于從本地加載锅睛,即使沒有網(wǎng)絡(luò),也能展示首屏頁面历谍。這個也就是大家熟悉的離線包现拒。
手 Q 使用 7Z 生成離線包, 同時離線包服務(wù)器將新的離線包跟業(yè)務(wù)對應(yīng)的歷史離線包進行 BsDiff 做二進制差分,生成增量包望侈,進一步降低下載離線包時的帶寬成本印蔬,下載所消耗的流量從一個完整的離線包(253KB)降低為一個增量包(3KB)。
經(jīng)過一系列優(yōu)化后脱衙,在 Android 平臺上侥猬,點擊到頁面首屏展示的耗時從平均 3s 多降低為 1.8s,優(yōu)化 40% 以上捐韩。
VasSonic 的誕生
雖然通過靜態(tài)直出和離線預(yù)推等方式優(yōu)化后退唠,速度已經(jīng)達到 1.8s,但還存在很大的優(yōu)化空間奥帘,當(dāng)我們準備持續(xù)深入優(yōu)化時铜邮,我們的業(yè)務(wù)形態(tài)發(fā)生了新的變化。
之前我們頁面內(nèi)容的數(shù)據(jù)主要是由產(chǎn)品經(jīng)理要配置的寨蹋,用戶看到的內(nèi)容基本都是一樣的松蒜。而現(xiàn)在頁面為了更好地為用戶推薦喜歡的內(nèi)容,我們后臺引入機器學(xué)習(xí)和隨機算法來做智能個性化推薦已旧。比如左邊新用戶推薦的是新貨精選秸苗,而右邊活躍用戶展示的是潮品推薦。另外還有部分的內(nèi)容是隨機算法推薦的运褪。這意味著不同用戶看到的內(nèi)容是不同的惊楼,同一個用戶不同時間看到的內(nèi)容也有可能不同玖瘸。
所以為了滿足業(yè)務(wù)的需求,我們只能實時拉取用戶數(shù)據(jù)并在服務(wù)端渲染后返回給客戶端檀咙,也就是動態(tài)直出的方案雅倒。
但是動態(tài)直出方案存在幾個比較明顯的問題:
服務(wù)端實時拉取數(shù)據(jù)渲染導(dǎo)致白屏?xí)r間長,因為服務(wù)器要先實時拉取個人數(shù)據(jù)弧可,然后進行渲染直出蔑匣,這個耗時不可控;
首屏無法使用離線預(yù)推等緩存策略棕诵,因為每個用戶看到的內(nèi)容不一樣裁良,我們無法通過靜態(tài)直出的方式那樣把 Html 全部發(fā)布到 CDN;
雖然動態(tài)直出方案下校套,頁面首屏無法通過離線預(yù)推等方式進行加載優(yōu)化价脾,但前面優(yōu)化積累的經(jīng)驗給我們提供了思路:要優(yōu)化白屏問題,核心還是得從提升資源加載速度方向入手笛匙。所以我們重點在資源加載方面進行了深度優(yōu)化侨把。
首先在加載流程方面,我們發(fā)現(xiàn)這里 WebView 訪問依然是串行的妹孙, WebView 要等終端初始化完成之后座硕,才發(fā)起請求。雖然終端耗時優(yōu)化了不少涕蜂,但是從外網(wǎng)的統(tǒng)計數(shù)據(jù)來看,終端初始化還是存在幾百毫秒的耗時映琳,而這段時間內(nèi)網(wǎng)絡(luò)是在空等的机隙。
因此性能上不夠極致,我們優(yōu)化代碼萨西,這兩個操作并行處理有鹿,流程改為:
并行處理后速度有所改善,但我們發(fā)現(xiàn)在某些場景下谎脯,終端初始化比較快葱跋,但數(shù)據(jù)沒有完成返回,這意味著內(nèi)核在空等源梭,而內(nèi)核是支持邊加載邊渲染的娱俺,我們在并行的同時,能否也利用內(nèi)核的這個特性呢废麻?
于是我們加入了一個中間層來橋接內(nèi)核和數(shù)據(jù)荠卷,內(nèi)部稱為流式攔截:
啟動子線程請求頁面主資源,子線程中不斷講網(wǎng)絡(luò)數(shù)據(jù)讀取到內(nèi)存中烛愧,也就是網(wǎng)絡(luò)流 (NetStream) 和內(nèi)存流 (MemStream) 之間的轉(zhuǎn)換油宜;
當(dāng) WebView 初始化完成的時候掂碱,提供一個中間層 BridgeStream 來連接 WebView 和數(shù)據(jù)流;
當(dāng) WebView 讀取數(shù)據(jù)的時候慎冤,中間層 BridgeStream 會先把內(nèi)存的數(shù)據(jù)讀取返回后疼燥,再繼續(xù)讀取網(wǎng)絡(luò)的數(shù)據(jù)。
通過這種橋接流的方式蚁堤,整個內(nèi)核無需等待醉者,繼續(xù)做到邊加載邊解析。這種并行的方式讓首屏的速度優(yōu)化 15% 以上违寿,進一步提升了頁面加載速度湃交。
通過并行加載,我們極大地提升了 WebView 請求的速度藤巢,但是在弱網(wǎng)絡(luò)場景下白屏?xí)r間還是非常長搞莺,用戶體驗非常糟糕。于是我們在思考掂咒,是否能夠?qū)⒂脩舻囊呀?jīng)加載的頁面內(nèi)容緩存下來才沧,等用戶下此點擊頁面的時候,我們先加載展示頁面緩存绍刮,第一時間讓用戶看到內(nèi)容温圆,然后同時去請求新的頁面數(shù)據(jù),等新的頁面數(shù)據(jù)拉取下來之后孩革,我們再重新加載一遍即可岁歉。
保存頁面內(nèi)容這個工作很簡單,因為現(xiàn)在我們資源讀取都是通過中間層 BridgeStream 來管理的膝蜈,只需要將整個讀取的內(nèi)容緩存下來即可锅移。
于是我們就按動態(tài)緩存這種方案去實現(xiàn)了,但很快就發(fā)現(xiàn)了問題饱搏。用戶打開頁面之后非剃,先是看到歷史頁面,等用戶準備去操作的時候推沸,突然頁面白閃一下备绽,重新加載了一遍,這種體驗非常差鬓催,特別在一些低端機器上肺素,這個白閃的過程太明顯,非常影響體驗宇驾,這是用戶和產(chǎn)品經(jīng)理都不能接受的压怠。于是我們在思考,能否只做局部的刷新飞苇,僅刷新變化的元素呢菌瘫?
通過分析蜗顽,我們發(fā)現(xiàn)同一個用戶的頁面,大部分數(shù)據(jù)都是不變的雨让,經(jīng)常變化的只有少量數(shù)據(jù)雇盖,于是我們提出了模板 (template) 和數(shù)據(jù)塊 (data) 的概念:頁面中經(jīng)常變化的數(shù)據(jù)我們稱為數(shù)據(jù)塊,除了數(shù)據(jù)塊之外的數(shù)據(jù)稱為模板栖忠。
我們將整個頁面 html 通過 VasSonic 標(biāo)簽進行劃分崔挖,包裹在標(biāo)簽中的內(nèi)容為 data,標(biāo)簽外的內(nèi)容為模版庵寞。
首先我們對 Html 內(nèi)容進行了擴展狸相,通過代碼注釋的方式,增加了“sonicdiff-xxx”來標(biāo)注一個數(shù)據(jù)塊的開始與結(jié)束捐川。
而模板就是將數(shù)據(jù)塊摳掉之后的 Html脓鹃,然后通過{albums}來表示這個是一個數(shù)據(jù)塊占位。
數(shù)據(jù)就是 JSON 格式古沥,直接 Key-Value瘸右。
當(dāng)然,為了完美地兼容 Html岩齿,我們對協(xié)議頭部進行了擴展太颤,比如增加 accept-diff 來標(biāo)注是否支持增量更新、template-tag 來標(biāo)注模板的 md5 是多少等盹沈。OK龄章,有了上面這個規(guī)則或者公式后,我們就可以實現(xiàn)增量更新了乞封。
VasSonic 為了支持區(qū)分客戶端是否支持增量更新等能力瓦堵,對頭部字段進行了擴展。
?
cache-offline 字段說明
列表如下:
?
模式介紹
VasSonic 根據(jù)本地是否有緩存以及本地緩存數(shù)據(jù)跟服務(wù)器數(shù)據(jù)的差異情況分為以下四種模式歌亲。
首次加載
我們會在請求頭部帶上支持 accept-diff 為 true 和 sdk 版本號等標(biāo)識著首次加載的信息。當(dāng)請求返回后澜驮,VasSonic 會在延遲幾秒后 (避免激烈 IO 競爭) 將頁面抽離成模板和數(shù)據(jù)并保存到本地陷揪。此時終端緩存目錄下,該頁面將對應(yīng)三個緩存文件 xxx.html杂穷、xxx.template悍缠、xxx.data,其中 xxx 是該頁面的唯一標(biāo)識 (即 sonicSessionId)耐量。
對于頁面非首次加載場景飞蚓,VasSonic 優(yōu)先加載本地緩存, 同時我們會在請求頭部帶上當(dāng)前緩存和模板的 md5廊蜒,后臺進行模板 md5 對比之后趴拧,分為以下幾種情況:
非首次加載之完全緩存
本地有緩存溅漾,且緩存內(nèi)容跟服務(wù)器內(nèi)容完全一樣。
非首次加載之增量數(shù)據(jù)
如果模板發(fā)現(xiàn)沒有變化著榴,那么會在響應(yīng)頭部返回 template-change=false添履,同時響應(yīng)包體返回的數(shù)據(jù)不再是完整的 html,而是一段 JSON 數(shù)據(jù)脑又,及全部的數(shù)據(jù)塊暮胧。我們現(xiàn)在需要跟本地數(shù)據(jù)進行差分,找出真正的增量數(shù)據(jù)问麸,如上圖中往衷,后臺返回了 N 個數(shù)據(jù),實際上僅有一個數(shù)據(jù)是有變化的严卖,那么我們僅需要將這個變化的數(shù)據(jù)提交到頁面即可席舍。一般場景下,這個差異的數(shù)據(jù)比全部數(shù)據(jù)要小很多妄田。如果頁面拆分數(shù)據(jù)得更細俺亮,那么頁面的變動就更小,這個取決于前端同學(xué)對數(shù)據(jù)塊的細化程度疟呐。
獲得變化數(shù)據(jù)塊 (diff_data) 后脚曾,客戶端只需要通知頁面頁面設(shè)置的回調(diào)接口 (getDiffDataCallback) 進行界面元素更新即可。這里 javascript 的通信方式也可以自由定義 (可以使用 webview 標(biāo)準的 javascript 通信方式启具,也可以使用偽協(xié)議的方式)本讥,只要頁面跟終端協(xié)商一致就可以。
對于數(shù)據(jù)更新這種場景鲁冯,終端還會將新的數(shù)據(jù)和模板拼接成為新的頁面拷沸,保持緩存最新。當(dāng)終端初始化比較慢的時候薯演,WebView 去加載緩存的時候撞芍,這個頁面可能已經(jīng)是最新的了,連數(shù)據(jù)刷新都不需要跨扮。
非首次加載之模板更新
與數(shù)據(jù)更新模式不一樣序无,由于業(yè)務(wù)需求,頁面的模板會發(fā)生更改衡创。當(dāng)終端在獲取到新的模板和數(shù)據(jù)后帝嗡,本地在子線程中進行合并,生成一個新的緩存璃氢,然后回調(diào)通知終端哟玷,刷新 WebView 來加載新的緩存。
我們來看一下最終的流程圖一也,跟動態(tài)緩存對比巢寡,有不少細節(jié)優(yōu)化:
我們從第 2 步開始喉脖,SonicSession 首先會去讀取緩存。會拋個消息通知 WebView 讀取緩存讼渊,如果 Webview 已經(jīng)準備好动看,則直接加載緩存,如果沒有爪幻,則緩存先放在內(nèi)存里面菱皆。同時 SonicSession 也會帶上模板等信息到后臺拉取新的內(nèi)容,后臺經(jīng)過 Sonic-Diff 之后挨稿,會返回新的數(shù)據(jù)仇轻。SonicSession 拿到新的數(shù)據(jù)后,首先會跟本地數(shù)據(jù)進行 Diff奶甘,如果發(fā)現(xiàn) WebView 已經(jīng)加載緩存篷店,則直接提交增量數(shù)據(jù)給頁面。否則繼續(xù)拼接最新的頁面臭家,替換掉內(nèi)存里面的緩存疲陕,同時保存到本地。這個時候 WebView 如果 Ready钉赁,則直接進行第 5 步 load 最新的內(nèi)容即可蹄殃。
效果統(tǒng)計
這個是我們外網(wǎng)的統(tǒng)計數(shù)據(jù)。在數(shù)據(jù)更新模式下你踩,首屏的耗時在 1s 左右诅岩,相比普通的動態(tài)直出,優(yōu)化了 50% 以上带膜。模板更新這個會比首次高吩谦,是因為加載了兩次頁面,不過從模式占比上來看膝藕,我們大部分頁面都是數(shù)據(jù)更新式廷。針對模板更新這種耗時比較高的情況,前面優(yōu)化積累的經(jīng)驗給我們提供了思路芭挽,核心還是從提前獲取資源方向入手滑废,因此我們優(yōu)先考慮如何預(yù)加載模板更新。
預(yù)加載
實際上整個 SonicSession 在沒有 WebView 的情況下览绿,也是可以獨立完成所有邏輯的,當(dāng)用戶點擊頁面的時候穗慕,我們在將 WebView 和 SonicSession 綁定起來即可饿敲。于是我們支持了兩種預(yù)加載的模式,一種是通過后臺 push 的方式逛绵,來提前獲取數(shù)據(jù)怀各。還有一種就是 JSAPI倔韭,頁面可以調(diào)用 JSAPI 來預(yù)加載用戶可能操作的下一個頁面。通過這兩種方式瓢对,我們可以把需要的增量更新數(shù)據(jù)提前拉取回來
展望未來
開源只是故事的開始寿酌,我們?nèi)詴掷m(xù)對 VasSonic 做改進,包括更易用的接口硕蛹、更好的性能醇疼、更高的可靠性,同時快速響應(yīng)解決開源后的 issue 和 PR法焰。這些改進最終也會原封不動地在手 Q 內(nèi)使用秧荆,這一切都是為了更快的 WebView 加載速度。
Talk is cheap埃仪,read the fucking code. If you are interested in VasSonic, don't forget to STAR VasSonic.
Thank you for reading ~