(資料圖)
導(dǎo)讀|H5開屏龜速常是令開發(fā)者頭疼的問題。騰訊企業(yè)微信團(tuán)隊(duì)對(duì)該現(xiàn)象進(jìn)行分析優(yōu)化,最終H5開屏耗時(shí)130ms,達(dá)到秒開效果!企微前端開發(fā)工程師陳智仁將分享可用可擴(kuò)展的Hybird H5秒開方案。該團(tuán)隊(duì)使用離線包解決了資源請求耗時(shí)的問題,在這個(gè)基礎(chǔ)上通過耗時(shí)分析找到瓶頸環(huán)節(jié),進(jìn)一步采用“預(yù)熱”進(jìn)行優(yōu)化提速以解決了WebView初始化、數(shù)據(jù)預(yù)拉取、js執(zhí)行(app初始化)耗時(shí)的問題。希望這些通用方法對(duì)你有幫助。
背景
服務(wù)端渲染(SSR)是Web主流的性能優(yōu)化手段。SSR直出相比傳統(tǒng)的SPA應(yīng)用加載渲染規(guī)避了首屏拉取數(shù)據(jù)和資源請求的網(wǎng)絡(luò)往返耗時(shí)。團(tuán)隊(duì)針對(duì)Web開發(fā)也已經(jīng)支持了SSR能力。近期出于動(dòng)態(tài)化運(yùn)營的考慮,我們選擇了Web開發(fā),同時(shí)我們也接到了提升體驗(yàn)的訴求。以企業(yè)微信要開發(fā)的頁面為例:采用SSR方案,從用戶點(diǎn)擊到首屏渲染的耗時(shí)均值約600ms,白屏?xí)r間的存在是可以感知到的。為了盡可能消除白屏達(dá)到秒開效果,我們嘗試做更多探索。方案思路1) 方案選型如何實(shí)現(xiàn)頁面秒開呢?從最直觀的渲染鏈路來入手分析。下圖列出了從用戶點(diǎn)擊到看到首屏渲染可交互,一個(gè)SPA應(yīng)用主要環(huán)節(jié)的加載流程。我們調(diào)研了業(yè)內(nèi)相關(guān)方案,從渲染鏈路的視角來看下常見方案的優(yōu)化思路。傳統(tǒng)離線包在加載渲染過程中,網(wǎng)絡(luò)IO是很明顯的一個(gè)耗時(shí)瓶頸。傳統(tǒng)的離線包方案思路很直接,如果網(wǎng)絡(luò)耗時(shí)那就將資源離線,很好地解決了資源請求的耗時(shí)。用Service Worker也能達(dá)到離線包的效果,同時(shí)也是Web標(biāo)準(zhǔn)。首次渲染優(yōu)化一般需要結(jié)合客戶端配置預(yù)啟動(dòng)腳本來達(dá)到緩存資源的效果。SSRSSR則從另外的角度出發(fā),在請求頁面的時(shí)候就進(jìn)行服務(wù)端數(shù)據(jù)拉取和頁面直出,首屏得以在一個(gè)網(wǎng)絡(luò)往返就可以展示,有效地規(guī)避了后續(xù)需要等待css/js資源加載、數(shù)據(jù)拉取的時(shí)間。性能體驗(yàn)有比較大的提升,在BFF普及的情況下開發(fā)模式簡單,很受歡迎。公司內(nèi)相關(guān)工作考慮到WebView的初始化(冷啟動(dòng)/ 二次啟動(dòng))、頁面網(wǎng)絡(luò)請求、首屏數(shù)據(jù)接口的耗時(shí),白屏?xí)r間還是可感知地存在的。以我們要開發(fā)的頁面為例采用SSR首屏耗時(shí)均值~600ms,可交互時(shí)間均值~1100ms。如何進(jìn)一步消除白屏?這里為各位介紹公司內(nèi)外針對(duì)h5首屏性能優(yōu)化的優(yōu)秀方案。手Q團(tuán)隊(duì)的VasSonic是集大成者,主要思路是采用WebView和數(shù)據(jù)預(yù)拉取并行的方式。這套方案需要客戶端和服務(wù)端采用指定協(xié)議改造接入,開發(fā)時(shí)也有一定的改造工作。微信游戲團(tuán)隊(duì)主要思路是利用jsCore做客戶端預(yù)渲染,用戶點(diǎn)擊后直接上屏。這個(gè)方法也達(dá)到了很好的效果,首屏FCP時(shí)間從1664ms降低到了411ms。我們做了一個(gè)簡要的方案對(duì)比,可以看到每個(gè)方案都針對(duì)渲染鏈路的某個(gè)或多個(gè)環(huán)節(jié)做了優(yōu)化,其中VasSonic的效果比較顯著。不過結(jié)合企業(yè)微信業(yè)務(wù)實(shí)際情況,我們列出了如下幾點(diǎn)考慮:首先,接入對(duì)客戶端和服務(wù)端有一定的改造成本,業(yè)務(wù)開發(fā)也有一定的改造工作。其次,我們已經(jīng)有一套的統(tǒng)一發(fā)布平臺(tái),希望能復(fù)用這套發(fā)布能力。最后,性能上有沒有進(jìn)一步優(yōu)化的空間呢?業(yè)務(wù)需求對(duì)體驗(yàn)上的要求是希望達(dá)到更好的性能效果或者說盡可能完全地消除白屏。基于以上考慮,我們在上述方案的基礎(chǔ)上做了進(jìn)一步的實(shí)踐探索,以期望達(dá)到更好的性能效果。
離線包 | SSR | VasSonic | CSR |
資源加載 |
數(shù)據(jù)拉取 |
JS執(zhí)行 |
WebView啟動(dòng)優(yōu)化 |
首屏FCP |
可交互(取決于JS執(zhí)行) |
2)方案架構(gòu)為了達(dá)到盡可能完全消除白屏,我們還是從初始問題出發(fā),結(jié)合渲染鏈路進(jìn)行分析,思路上針對(duì)每個(gè)環(huán)節(jié)采取對(duì)應(yīng)的優(yōu)化方法。每個(gè)環(huán)節(jié)的優(yōu)化在具體落地時(shí)會(huì)存在著方案的利弊取舍。比如預(yù)拉取數(shù)據(jù)一般的思路是交給客戶端來做,但是存在著客戶端請求和h5請求兩套機(jī)制(鑒權(quán)、請求通道等方面)如何協(xié)調(diào)的問題。在渲染鏈路分析時(shí),如果業(yè)務(wù)的js執(zhí)行也貢獻(xiàn)了不少耗時(shí),有沒有可能從通用基礎(chǔ)方案的角度來解決這個(gè)問題,同時(shí)也能減少業(yè)務(wù)對(duì)性能優(yōu)化的關(guān)注?這是個(gè)值得各位思考探索的問題。具體的內(nèi)容會(huì)在后面展開來說。如圖展示了方案的優(yōu)化思路和主流程。方案使用離線包解決了資源請求耗時(shí)的問題,在這個(gè)基礎(chǔ)上通過耗時(shí)分析找到瓶頸環(huán)節(jié),進(jìn)一步采用預(yù)熱的思路進(jìn)行優(yōu)化提速,解決了WebView初始化、數(shù)據(jù)預(yù)拉取、js執(zhí)行(app初始化)耗時(shí)的問題,最終達(dá)到了理想的性能體驗(yàn)。圖1 上屏流程圖2 方案架構(gòu)下面我們具體介紹下方案,包括:離線包技術(shù)、預(yù)熱提速和進(jìn)一步的優(yōu)化工作。離線包加速為了規(guī)避資源請求耗時(shí),我們使用了離線包技術(shù)。離線包技術(shù)是比較成熟的方案,相關(guān)打包、發(fā)布拉取的方案這里不多說了,主要說下方案中一些設(shè)計(jì)上的考量。1)加載流程我們通過offid作為離線包應(yīng)用的標(biāo)識(shí),fallback機(jī)制保證離線資源不可達(dá)時(shí)用戶也可以正常訪問頁面,通過離線包預(yù)拉取和異步檢測更新機(jī)制提高了離線包命中率,盡可能消除了網(wǎng)絡(luò)資源加載的耗時(shí)。2)fallback機(jī)制因?yàn)橛脩艟W(wǎng)絡(luò)狀況的不確定性,離線包加載可能存在失敗的情況。為了保證可用性,我們確定了離線包加載不阻塞渲染的思路。當(dāng)用戶點(diǎn)擊入口url,對(duì)應(yīng)offid離線包在本地不存在時(shí),會(huì)fallback請求現(xiàn)網(wǎng)頁面,同時(shí)異步加載離線包。所以我們針對(duì)離線包的打包結(jié)構(gòu),按照現(xiàn)網(wǎng)URL path來組織資源路徑。這樣客戶端請求攔截處理也會(huì)比較方便,不需要理解映射規(guī)則。當(dāng)發(fā)現(xiàn)離線包不匹配資源時(shí),放過請求透到現(xiàn)網(wǎng)即可。如圖展示了我們的離線包結(jié)構(gòu)示例。3. 離線包生命周期為了提高離線包命中率,我們會(huì)配置一些時(shí)機(jī)(e.g.入口曝光)來預(yù)拉取離線包。離線包的更新機(jī)制:客戶端加載時(shí)根據(jù)offid檢測到本地離線包的存在,則直接使用拉起,同時(shí)啟動(dòng)異步版本檢測和更新。如果新包版本號(hào)大于本地版本號(hào)則更新緩存,同時(shí)發(fā)布平臺(tái)也支持區(qū)分測試環(huán)境、正式環(huán)境以及按條件灰度。上了離線包后,可以看到頁面的首屏耗時(shí)均值從基準(zhǔn)無優(yōu)化的1340ms降到了963ms,離線包的預(yù)拉取和更新策略則使離線包命中率達(dá)到了95%。首屏耗時(shí)得到了一定的降低,但也還有比較大的優(yōu)化空間,需要更一步的分析優(yōu)化。預(yù)熱提速通過離線包的加速,我們解決了資源請求耗時(shí)的問題,不過從整個(gè)渲染鏈路來看還有很大的優(yōu)化空間,我們做了具體的耗時(shí)分析,找出耗時(shí)瓶頸,針對(duì)耗時(shí)環(huán)節(jié)做了進(jìn)一步的優(yōu)化提速。1)耗時(shí)分析離線包技術(shù)規(guī)避了資源請求耗時(shí),但是從整個(gè)渲染鏈路來看還有很大的優(yōu)化空間,我們做了耗時(shí)分析如下。Hybird應(yīng)用中,WebView初始化是比較耗時(shí)的環(huán)節(jié),這里我們針對(duì)iOS WebView做了測試。
首次冷啟動(dòng)/ms | 二次打開/ms |
iOS(WKWebView) | 480ms | 90ms |
數(shù)據(jù)拉取方面,不同入口頁面的耗時(shí)不一,某些入口頁面比較重的接口耗時(shí)超過了1s。此外,我們發(fā)現(xiàn)js執(zhí)行也貢獻(xiàn)了不少耗時(shí)。以某入口頁面為例,框架初始化時(shí)間~10ms,app初始化時(shí)間~440ms。2)渲染鏈路預(yù)熱提速預(yù)熱流程我們的目標(biāo)是消除白屏,這里理想的方案是找到一種和業(yè)務(wù)無關(guān)的通用解法。方案的主要思路是預(yù)熱,把能提前做的都做了。預(yù)熱是不是就是把WebView提前創(chuàng)建出來就好了呢?不是的,這里的預(yù)熱涉及到多個(gè)渲染環(huán)節(jié)的優(yōu)化組合。如圖展示了預(yù)熱的整體流程,下面一個(gè)個(gè)來解。2)WebView預(yù)創(chuàng)建為了消除WebView的耗時(shí),我們采取了全局的預(yù)創(chuàng)建WebView,時(shí)機(jī)為配置入口曝光。不過全局復(fù)用預(yù)熱WebView不可避免地會(huì)引入可能的業(yè)務(wù)內(nèi)存泄露問題,下文會(huì)介紹對(duì)應(yīng)的規(guī)避方案。數(shù)據(jù)預(yù)拉取數(shù)據(jù)拉取是頁面渲染的一個(gè)耗時(shí)環(huán)節(jié)。為了消除數(shù)據(jù)預(yù)拉取耗時(shí),在預(yù)創(chuàng)建WebView階段我們同時(shí)進(jìn)行了數(shù)據(jù)預(yù)拉取。數(shù)據(jù)預(yù)拉取常見的思路是交給客戶端來做,但是存在著客戶端請求和h5請求兩套機(jī)制如何協(xié)調(diào)的問題,以請求鑒權(quán)為例,存在以下的問題:第一,Web團(tuán)隊(duì)自身有一層node BFF,實(shí)現(xiàn)了相應(yīng)的數(shù)據(jù)拉取業(yè)務(wù)邏輯,而客戶端則走的私有協(xié)議通道請求C++后臺(tái),二者是不同的鑒權(quán)機(jī)制。第二,如果交給客戶端來做,可以接入HTTP請求這套機(jī)制,改造成本比較大,如果復(fù)用原有通道,則一份數(shù)據(jù)業(yè)務(wù)邏輯需要兩套實(shí)現(xiàn)。如何設(shè)計(jì)一套通用可擴(kuò)展的方案?我們希望做到客戶端只關(guān)注容器的能力(預(yù)熱、資源攔截等),屏蔽掉更深入的對(duì)Web的感知,這樣的解耦可以有效控制方案的復(fù)雜度。因此,這里我們針對(duì)離線包配置項(xiàng)增加了preUrl字段,使客戶端維護(hù)更通用的能力,數(shù)據(jù)預(yù)拉取交給業(yè)務(wù)團(tuán)隊(duì)來做,具體如下:第一,客戶端:拉取某個(gè)離線包配置項(xiàng)時(shí)會(huì)讀取該字段,同時(shí)針對(duì)當(dāng)前曝光的入口url可能存在多個(gè)有著不同的數(shù)據(jù)需求,這里會(huì)進(jìn)行收集,將曝光url中的業(yè)務(wù)key參數(shù)拼接到preUrl來初始化WebView,這些作為通用能力。第二,業(yè)務(wù):preUrl頁面在加載時(shí)會(huì)拉取相應(yīng)的業(yè)務(wù)數(shù)據(jù)存到localStorage,實(shí)際的數(shù)據(jù)預(yù)拉取請求放到業(yè)務(wù)方發(fā)起,也可以很好地兼容已有的技術(shù)棧。JS預(yù)執(zhí)行很接近目標(biāo)了,最后js執(zhí)行的耗時(shí)能不能消除呢?首先來看下440ms的耗時(shí)具體在哪里,通過分析看到,框架初始化僅需要不到10ms的時(shí)間,而真正的大頭在業(yè)務(wù)代碼的執(zhí)行,其中代碼編譯耗時(shí)~80ms,其余的都是業(yè)務(wù)app初始化執(zhí)行時(shí)間,這個(gè)是業(yè)務(wù)本身復(fù)雜度造成的。我們首先考慮了創(chuàng)建兩個(gè)WebView的方案,一個(gè)負(fù)責(zé)加載preUrl預(yù)拉取數(shù)據(jù),另一個(gè)負(fù)責(zé)loadUrl上屏,這樣設(shè)計(jì)上比較簡潔健壯,不過實(shí)踐下來發(fā)現(xiàn)效果不理想,如圖展示了該方案的效果,渲染不穩(wěn)定可以感知到白屏的存在。在已經(jīng)有了預(yù)拉取數(shù)據(jù)和離線資源的情況下,理論上用戶點(diǎn)擊后需要等待的就只有渲染這塊的耗時(shí),實(shí)際我們發(fā)現(xiàn)在復(fù)雜應(yīng)用初始化時(shí)存在js執(zhí)行耗時(shí)較大的問題。最終我們做了一個(gè)預(yù)執(zhí)行的解法。結(jié)合SPA的特點(diǎn),將preUrl作為SPA的一個(gè)子頁面,不需要UI展示,只負(fù)責(zé)預(yù)拉取數(shù)據(jù),這樣子頁面加載完成的同時(shí)也完成了app提前初始化。而相應(yīng)的不同入口切換頁面時(shí),不同于復(fù)用預(yù)熱WebView重新reload頁面,為了保留app初始化的效果,我們采取了一套Native通知Web SDK,頁面切換交給WebView控制的方案。其中,Native通知?jiǎng)t以調(diào)用SDK全局方法的方式。通過這種方式,入口頁面間切換其實(shí)只是hashchange觸發(fā)的子頁面渲染,達(dá)到了不錯(cuò)的效果。流程圖即預(yù)熱方案的上屏部分。該方案執(zhí)行后我們達(dá)到了預(yù)期目標(biāo)效果,最大限度地消除了白屏接近Native體驗(yàn)。需求上線后通過監(jiān)控?cái)?shù)據(jù)可以看到在命中預(yù)熱和離線包邏輯的情況下,從用戶點(diǎn)擊到頁面上屏可交互耗時(shí)均值約130ms。進(jìn)一步優(yōu)化1)離線包安全在離線包安全方面,為了防止包篡改,每我們次打包發(fā)布時(shí)都會(huì)生成包簽名和文件md5??蛻舳嗽谑褂媒馕鲭x線包時(shí)會(huì)校驗(yàn)完整性,在返回離線資源時(shí)會(huì)校驗(yàn)文件完整性。2)穩(wěn)定性整體方案在性能上已經(jīng)達(dá)到目標(biāo)了,保證穩(wěn)定性對(duì)產(chǎn)品體驗(yàn)也很重要。我們?yōu)榱讼齤s執(zhí)行的耗時(shí),采取了Native通知Web SDK控制頁面切換的方式。雖然比較靈活但是也帶來了穩(wěn)定性的問題。具體來說,如果SDK在做頁面切換時(shí)異常,之后用戶打開每個(gè)入口url都會(huì)看到相同的頁面。入口頁面的業(yè)務(wù)在用戶使用過程中如果跳轉(zhuǎn)了非SPA的鏈接同時(shí)沒有注入SDK,之后的頁面切換也會(huì)失效。如何保證預(yù)熱容器的可用性呢?我們設(shè)計(jì)了一套通知機(jī)制確??蛻舳烁兄筋A(yù)熱容器的可用狀態(tài),并在不可用時(shí)得以恢復(fù),如圖。預(yù)熱容器會(huì)維護(hù)isInit和isInvokedSuc兩個(gè)狀態(tài)。只有當(dāng)preUrl成功加載和SDK執(zhí)行成功上屏?xí)r,兩個(gè)狀態(tài)才會(huì)置true,此時(shí)的預(yù)熱WebView才是可用的,否則會(huì)回退到普通容器模式進(jìn)行l(wèi)oad url來加載頁面。此外,在每次入口url曝光時(shí),已有的預(yù)熱容器也會(huì)銷毀重建,也有效保證了容器的穩(wěn)定性。3)內(nèi)存泄露使用全局的預(yù)創(chuàng)建WebView,不可避免的會(huì)引入可能的業(yè)務(wù)內(nèi)存泄露問題。在測試過程中,我們也發(fā)現(xiàn)了這種例子??梢钥吹疆?dāng)點(diǎn)開使用了預(yù)熱容器的頁面后放置一段時(shí)間,整個(gè)內(nèi)存在不斷上漲,最終會(huì)導(dǎo)致PC端頁面的白屏或者移動(dòng)端的Crash,這個(gè)狀況最終歸因是業(yè)務(wù)邏輯的實(shí)現(xiàn)存在缺陷。不過在基礎(chǔ)技術(shù)的角度而言,開發(fā)者也需要采取措施來盡可能規(guī)避內(nèi)存泄露的情況。主要思路是減少同一個(gè)預(yù)熱容器的常駐,也就是對(duì)存活的容器設(shè)置有效期,在適當(dāng)?shù)臅r(shí)機(jī)檢查并清理過期容器,我們選擇的時(shí)機(jī)是App前后臺(tái)切換時(shí)。4)解決副作用出于性能考慮,我們選擇了通過Web SDK控制頁面的方案,同時(shí)使用了全局的預(yù)創(chuàng)建WebView。這帶來了副作用——當(dāng)頁面對(duì)容器做了全局的設(shè)置,可能會(huì)影響到下一個(gè)頁面的表現(xiàn)。比如:設(shè)置document.title、通過私有JSAPI設(shè)置了WebView導(dǎo)航欄的表......當(dāng)執(zhí)行這些操作時(shí),在下一個(gè)頁面也復(fù)用預(yù)熱容器的情況下,全局設(shè)置沒有得到清理重置或者覆蓋,用戶會(huì)看到上個(gè)頁面的表現(xiàn)。為了解決上述問題,業(yè)務(wù)可以在每個(gè)頁面主動(dòng)聲明需要的表現(xiàn)來覆蓋上個(gè)頁面的設(shè)置,理想的方法還是基礎(chǔ)技術(shù)來規(guī)避這個(gè)問題來保證業(yè)務(wù)開發(fā)的一致性。我們在SDK控制切換頁面時(shí),進(jìn)行了一系列的重置操作。此外,在Windows和Mac端,我們也設(shè)計(jì)了雙預(yù)熱WebView的方案來完全解決這個(gè)問題。每次使用時(shí)同時(shí)創(chuàng)建新容器,得以保證每次打開入口頁面都是使用新創(chuàng)建的容器。當(dāng)然,方案的另一面則是會(huì)帶來App內(nèi)存的上漲??偨Y(jié)我們從渲染鏈路入手,針對(duì)每個(gè)環(huán)節(jié)進(jìn)行分析優(yōu)化,最終沉淀了一套可用可擴(kuò)展的Hybird H5秒開方案。從渲染鏈路的角度來看,方案通過離線包和預(yù)熱一系列優(yōu)化,將用戶從點(diǎn)擊到可交互的時(shí)間縮短到了一個(gè)SPA路由切換上屏步驟的耗時(shí)。上線后我們監(jiān)控發(fā)現(xiàn),命中了預(yù)熱離線邏輯的頁面首屏耗時(shí)~130ms,相比于離線包、SSR都有優(yōu)勢,同時(shí)預(yù)熱離線容器命中率也達(dá)到了97%,達(dá)到了理想的體驗(yàn)效果。希望本篇對(duì)你有幫助。??
騰訊工程師技術(shù)干貨直達(dá):
1、算法工程師深度解構(gòu)ChatGPT技術(shù)
2、10分鐘!從架構(gòu)視角讀懂K8s
3、探秘微信業(yè)務(wù)優(yōu)化:DDD從入門到實(shí)踐
4、耗時(shí)減半?騰訊云OCR只做了3件事
關(guān)鍵詞:
的情況下
進(jìn)行分析