得物AppH5秒開優(yōu)化實(shí)戰(zhàn)
?
大廠技術(shù)??高級(jí)前端??Node進(jìn)階
點(diǎn)擊上方?程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
導(dǎo)讀
? ? ? ? ?一開始我們的H5頁(yè)面秒開率只有30%左右,現(xiàn)在我們的H5頁(yè)面秒開率達(dá)到了 75%。這中間巨大的差異究竟有哪些黑科技在里面?我們?yōu)槭裁匆鯤5頁(yè)面的秒開優(yōu)化?我們的秒開指標(biāo)是如何統(tǒng)計(jì)的?客戶端和H5是怎么配合做到 1+1>2的?監(jiān)控是如何發(fā)現(xiàn)H5頁(yè)面可優(yōu)化項(xiàng)的?我們又通過監(jiān)控發(fā)現(xiàn)了哪些可優(yōu)化的問題呢?
1. 背景
??????? H5秒開優(yōu)化是一個(gè)老生常談的問題,本文將逐步介紹如何通過客戶端 + H5 的優(yōu)化手段(1+1>2)把秒開從 30% 提升到 75% ?后續(xù)接口預(yù)請(qǐng)求、客戶端預(yù)渲染以及預(yù)加載2.0上線后還會(huì)再次助力指標(biāo)提升。
為什么要做優(yōu)化?
Global Web Performance Matters for ecommerce?的報(bào)告中指出:
47%的用戶更在乎網(wǎng)頁(yè)在2秒內(nèi)是否完成加載。
52%的在線用戶認(rèn)為網(wǎng)頁(yè)打開速度影響到他們對(duì)網(wǎng)站的忠實(shí)度。
每慢1秒造成頁(yè)面 PV 降低11%,用戶滿意度也隨之降低降低16%。
近半數(shù)移動(dòng)用戶因?yàn)樵?0秒內(nèi)仍未打開頁(yè)面從而放棄。
整體系統(tǒng)架構(gòu)圖:

2. 指標(biāo)選擇
????????首先講一下得物用來衡量秒開的指標(biāo) FMP,那為什么不選擇 FCP 或者 LCP 呢?FCP 只有要渲染就會(huì)觸發(fā),LCP 兼容性不佳,得物希望站在用戶的角度來衡量秒開這件事情,用戶從點(diǎn)擊打開一個(gè)WebView到首屏內(nèi)容完整的呈現(xiàn)出來的時(shí)間點(diǎn)就是得物定義的FMP觸發(fā)時(shí)機(jī)。
指標(biāo)清楚了之后,再來看一下完整的 FMP 包含哪些耗時(shí)。

接下來將分為兩大部分進(jìn)行介紹,客戶端優(yōu)化部分和H5 優(yōu)化部分。
3. 客戶端優(yōu)化
????????通過 HTML 預(yù)加載、HTML 預(yù)請(qǐng)求、離線包、接口預(yù)請(qǐng)求、鏈接?;睢㈩A(yù)渲染等手段提升頁(yè)面首屏打開速度,其中預(yù)加載、預(yù)請(qǐng)求、離線包分別可提升10%左右的秒開。
3.1 HTML預(yù)加載
????????通過配置由客戶端提前下載好HTML主文檔,當(dāng)用戶訪問時(shí)直接使用已經(jīng)下載好的HTML文檔,以此減少HTML網(wǎng)絡(luò)請(qǐng)求時(shí)間,從而提升網(wǎng)頁(yè)打開速度。


3.1.1 如何確定需要下載的頁(yè)面
????????前人栽樹后人乘涼,得物App有很多的資源位,banner、金剛位、中通位等,這些位置顯示什么內(nèi)容,早就已經(jīng)是智能推薦算法產(chǎn)出的了,那么就可以直接指定這些資源位進(jìn)行預(yù)加載。

3.1.2 頁(yè)面緩存管理
頁(yè)面被預(yù)加載之后,總不能一直不更新吧?那么什么時(shí)候更新頁(yè)面的緩存呢?
預(yù)加載的頁(yè)面存放于內(nèi)存中,關(guān)閉App緩存就會(huì)被清除
通過配置過期時(shí)間人為控制最大緩存時(shí)間
在頁(yè)面進(jìn)入后發(fā)起異步線程去更新HTML文檔。
被現(xiàn)實(shí)打臉:
????????但是在后面的灰度過程中被現(xiàn)實(shí)狠狠的教育了一頓,發(fā)現(xiàn)有些SSR的頁(yè)面會(huì)涉及到狀態(tài)的變更,比如說:領(lǐng)劵場(chǎng)景。這些狀態(tài)都是經(jīng)過SSR服務(wù)渲染好的,用戶在進(jìn)入頁(yè)面時(shí)還沒有領(lǐng)劵,這個(gè)時(shí)候去更新HTML文檔,實(shí)屬更新了個(gè)寂寞,在用戶領(lǐng)劵之后關(guān)閉頁(yè)面再次進(jìn)入,發(fā)現(xiàn)頁(yè)面中的狀態(tài)仍是讓用戶領(lǐng)劵,點(diǎn)擊領(lǐng)劵又告訴人家你已經(jīng)領(lǐng)過了。

改進(jìn)措施
H5 頁(yè)面在打開時(shí)針對(duì)狀態(tài)可能會(huì)發(fā)生變更的組件,再次請(qǐng)求接口獲取最新的狀態(tài)數(shù)據(jù)。
客戶端由進(jìn)入頁(yè)面就更新HTML文檔修改為:關(guān)閉WebView時(shí)更新HTML文檔。
3.1.3 線上收益效果
????????至此問題也解決了,工程師的任務(wù)結(jié)束了嗎?如果你認(rèn)為功能做上去就算結(jié)束,那么此時(shí)此刻請(qǐng)你一定要轉(zhuǎn)變思維,想一想我們的目標(biāo)是什么?我們的目標(biāo)是「提升秒開」,預(yù)加載只是一種提升秒開的手段,但是在功能做上去之后并不知道這個(gè)功能帶來了多少秒開的收益,因此在把功能開發(fā)完成上線之后,就要開始關(guān)注上線之后的結(jié)果,來看看預(yù)加載的性能表現(xiàn)如何。從下圖可以看到,預(yù)加載開啟狀態(tài)下可提升10%以上的秒開率。

3.1.4 遇到的挑戰(zhàn)
預(yù)加載的頁(yè)面基本上都是 SSR 服務(wù)的頁(yè)面,預(yù)加載無形中造成了大量的請(qǐng)求,此時(shí)得物的SSR服務(wù)扛不住這么大的請(qǐng)求量;
即使SSR 服務(wù)扛得住,也會(huì)對(duì)后端整個(gè)服務(wù)鏈路造成壓力。
(1)SSR服務(wù)擴(kuò)容
????????要解決服務(wù)器壓力問題,很自然就會(huì)想到增加機(jī)器,于是我們對(duì)SSR機(jī)器數(shù)量做了一次擴(kuò)容,將機(jī)器數(shù)量提升了一倍,這個(gè)時(shí)候繼續(xù)嘗試擴(kuò)大預(yù)加載的用戶數(shù)量,但是仍然無法抗住這么大的QPS,而且此時(shí)還引發(fā)了第二個(gè)問題,算法部門的服務(wù)器發(fā)出了告警,于是放量計(jì)劃又一次遇到了阻礙。
(2)破局者 CDN
????????利用CDN 服務(wù)器的緩存能力既可以減輕 SSR 服務(wù)器的壓力又可以減少后端服務(wù)鏈路的壓力,這么好的東西為什么不用呢?這里留個(gè)懸念,后面將H5優(yōu)化部分會(huì)詳細(xì)介紹。
(3)客戶端配合改造
????????支持針對(duì)CDN域名進(jìn)行全部開放預(yù)加載能力,針對(duì)非CDN域名保持原有放量比例。
3.1.5 開屏頁(yè)預(yù)加載
????
????????在這個(gè)過程中還分析了頁(yè)面的流量占比,發(fā)現(xiàn)開屏廣告來源的頁(yè)面流量占比也很高,那么是不是可以把開屏廣告的HTML文檔內(nèi)容也給預(yù)加載下來呢?
開屏頁(yè)面預(yù)加載策略
對(duì)預(yù)加載列表進(jìn)行去重,開屏廣告列表中可能會(huì)存在重復(fù)的頁(yè)面,他們的背景圖和生效時(shí)間是不同的;
增加了生效時(shí)間相關(guān)配置,開屏廣告列表中存在于未來某段時(shí)間才會(huì)展示的頁(yè)面;
添加黑白名單控制,開屏廣告列表中可能會(huì)有第三方合作頁(yè)面,他們不希望預(yù)加載統(tǒng)計(jì)會(huì)造成PV時(shí)不準(zhǔn)確。
3.1.6? 預(yù)加載展望
????????既然可以提前下載好HTML,那是不是可以更進(jìn)一步,提前把頁(yè)面內(nèi)的資源加載好,這樣在打開一個(gè)頁(yè)面的時(shí)候可以減少大部分的網(wǎng)絡(luò)請(qǐng)求從而更快速的把內(nèi)容呈現(xiàn)給用戶。這里還需要考慮如何跟下面講到的離線包進(jìn)行協(xié)作。

3.2 HTML預(yù)請(qǐng)求

????????在WebView初始化同時(shí),去請(qǐng)求HTML主文檔,等待HTML文檔下載完成 且 WebView初始成功后渲染,減少用戶等待時(shí)間,客戶端請(qǐng)求成功后,WebView加載本地 HTML,并保存以供下次使用。預(yù)請(qǐng)求HTML開啟狀態(tài)下可提升8%左右的秒開。
預(yù)請(qǐng)求 VS 預(yù)加載
????????
????????本質(zhì)上HTML預(yù)加載和HTML預(yù)請(qǐng)求的區(qū)別就是下載HTML文檔的時(shí)機(jī)不同, 預(yù)加載是在App啟動(dòng)后用戶無任何操作的情況下就會(huì)去下載,但是預(yù)請(qǐng)求只會(huì)在用戶單擊打開H5頁(yè)面的時(shí)候才會(huì)去下載。如果用戶是第二次打開某個(gè)H5頁(yè)面,此時(shí)發(fā)現(xiàn)本地有已經(jīng)下載好的HTML且尚未過期就會(huì)直接使用,這個(gè)時(shí)候的行為表現(xiàn)就跟預(yù)加載的功能是一致的了。
3.2.1 遇到挑戰(zhàn)
上線之后發(fā)現(xiàn)預(yù)請(qǐng)求只提升了2%左右的秒開,經(jīng)過分析發(fā)現(xiàn)問題:
緩存有效時(shí)間太短,頁(yè)面過期時(shí)間只配置了10分鐘,也就是說在10分鐘之后用戶就要重新去下載一次,那能不能把緩存時(shí)間延長(zhǎng)呢?
H5頁(yè)面是沒有自更新能力,無法支持配置更長(zhǎng)的緩存時(shí)間,跟預(yù)加載HTML問題一致。
3.2.2? 深入挖掘
????????在本地用低端機(jī)對(duì)整個(gè)秒開耗時(shí)鏈路進(jìn)行了分析,為什么要用低端機(jī)分析呢?低端機(jī)有個(gè)好處,天然的加上了慢放功能,可以最大程度發(fā)現(xiàn)問題。
(1)安卓 h5 頁(yè)面加載 與 原生布局填充并行執(zhí)行

????????從圖中可以看出h5 頁(yè)面加載之前 耗時(shí) 分布在 activityStart() 函數(shù),該函數(shù) 包含了 onCreateView ,其中耗時(shí)最長(zhǎng)是 布局填充 inflate(),因?yàn)?WebView 對(duì)象是提前創(chuàng)建好的,直接從對(duì)象池中取出的,所以耗時(shí)主要在 初始化過程,WebView 自身的初始化 WebViewChromiumFactoryProvider. startYourEngines (耗時(shí) 87 us,不到 1 ms),耗時(shí)還有 WebView 的一些其他初始化,jockey 的初始化 等等。
????????而秒開的計(jì)算是包含了 View 初始化到 WebVIew url 加載 的耗時(shí),從而發(fā)現(xiàn)了優(yōu)化點(diǎn),可以將Webview loadUrl 前置,h5 頁(yè)面加載 與 原生布局填充并行執(zhí)行。在 onCreateView 時(shí),創(chuàng)建 FrameLayout 進(jìn)行返回,執(zhí)行 WebView loadUrl 之后,主線程開始 對(duì)布局進(jìn)行 inflate,布局加載成功后,將其 addView 到 FrameLayout 中,減少了 loadUrl 的阻塞時(shí)長(zhǎng)。中高端機(jī)型有 15ms 左右優(yōu)化,低端機(jī)型有 30 ~ 50 ms 優(yōu)化 效果。
(2)雙端下載HTML的時(shí)機(jī)提前至路由階段
????????預(yù)請(qǐng)求HTML時(shí)機(jī)是在進(jìn)入到 native 頁(yè)面中,這個(gè)時(shí)間點(diǎn)距離用戶單擊事件已經(jīng)過去100ms,那么是否可以將下載HTML的時(shí)機(jī)提前呢?經(jīng)過一番探索,最終選擇在路由階段進(jìn)行攔截,既可以統(tǒng)一收口而且距離用戶點(diǎn)擊的時(shí)間間隔可以忽略不計(jì)。通過這種方式將下載HTML時(shí)機(jī)提前了平均80ms+。
此時(shí)的流程變成了下面這樣。

????????可能有的同學(xué)會(huì)問了,為什么不在用戶點(diǎn)擊的時(shí)候去下載呢?從用戶點(diǎn)擊到路由肯定還是有耗時(shí)的。
代碼層面不好維護(hù),如果在點(diǎn)擊時(shí)就需要侵入到業(yè)務(wù)層面,入口千千萬,很難進(jìn)行維護(hù)管理;
從點(diǎn)擊到路由這部分耗時(shí)在線下進(jìn)行了性能測(cè)試,幾乎可以忽略不計(jì)。
3.2.3? 最終線上收益效果
????????在上述問題解決后,將緩存時(shí)間修改為1天,發(fā)現(xiàn)預(yù)請(qǐng)求HTML開啟狀態(tài)下可提升8%左右的秒開,已經(jīng)和預(yù)加載的效果相差不大了。

3.3? 離線包
????????通過提前將H5頁(yè)面內(nèi)所需的css、js等資源聚合在一個(gè)壓縮包內(nèi),由客戶端在App啟動(dòng)后進(jìn)行下載解壓縮,在后續(xù)訪問H5頁(yè)面時(shí),匹配是否有本地離線資源,從而加速頁(yè)面訪問速度。

3.3.1 安卓實(shí)現(xiàn)
????????資源攔截這塊安卓這邊實(shí)現(xiàn)比較簡(jiǎn)單,WebView支持 shouldInterceptRequest, 可以在該方法內(nèi)檢測(cè)是否需要進(jìn)行資源攔截,需要的話返回 WebResourceResponse 對(duì)象,不需要直接返回 null。


3.3.2 iOS 實(shí)現(xiàn)
但是在iOS 這邊遇到了一些困難,調(diào)研了以下方案:
方案一:NSURLProtocol 攔截方式
? ???NSURLProtocol 攔截方式,使用WKBrowsingContextController和registerSchemeForCustomProtocol。通過反射的方式拿到了私有的 class/selector。通過把注冊(cè)把 http 和 https 請(qǐng)求交給 NSURLProtocol 處理。通過這種方式確實(shí)可以攔截請(qǐng)求,但是發(fā)現(xiàn)post請(qǐng)求的body會(huì)出現(xiàn)丟失的問題。而且NSURLProtocol一經(jīng)注冊(cè)就是全局開啟。我們希望他只會(huì)攔截接入了離線包的頁(yè)面,但是沒有辦法控制他,他會(huì)攔截所有頁(yè)面的請(qǐng)求,包括第三方合作頁(yè)面,顯然這是無法接受的。
方案二:通過CustomProtocol攔截請(qǐng)求
????????在iOS 11及以上系統(tǒng)中, 擁有了加載自定義資源的API:WKURLSchemeHandler。
可以修改當(dāng)前頁(yè)面url為自定義 scheme 協(xié)議,比如:https://fast.dewu.com 修改為 duapp://fast.dewu.com 然后在客戶端內(nèi)注冊(cè)該 scheme,前端配合修改頁(yè)面內(nèi)所有的資源請(qǐng)求未自適應(yīng)協(xié)議,如:src="http://fast.dewu.com/xxx" 就可以實(shí)現(xiàn)攔截。但是在測(cè)試過程中發(fā)現(xiàn),接口為了安全起見只允許白名單內(nèi)的域名發(fā)起跨域請(qǐng)求,且無法配置多個(gè)域名,導(dǎo)致該方案無法繼續(xù)進(jìn)行。
方案三:hook handlesURLScheme

????????仍然是使用 WKURLSchemeHandler 然后通過 hook WKWebview 的 handlesURLScheme 方法來支持 http 和 https 請(qǐng)求的代理。通過這種方式雖然可以攔截請(qǐng)求了,但是遇到了以下問題:
(1)body丟失問題
????????不過在 iOS 11.3 之后對(duì)這種情況做了修復(fù)處理,只有 blob 類型的數(shù)據(jù)會(huì)丟失。需要由JS來代理 fetch 和 XMLHttpRequest 的行為,在請(qǐng)求發(fā)起時(shí)將 body 內(nèi)容通過 JSBridge 告知 native,并將請(qǐng)求交給客戶端進(jìn)行發(fā)起,客戶端在請(qǐng)求完成后 callback js 方法。
(2)Cookie 丟失、無法使用問題
????????通過代理 document.cookie 賦值和取值動(dòng)作,交由客戶端來進(jìn)行管理,但是這里需要額外注意一點(diǎn),需要做好跨域校驗(yàn),防止惡意頁(yè)面對(duì)cookie進(jìn)行修改。
3.3.3 遇到挑戰(zhàn)
????????至此功能開發(fā)完成上線,先來一組線上收益數(shù)據(jù),安卓開啟離線情況有有10%左右的收益,但是iOS開啟離線的反而秒開率更低。經(jīng)過修復(fù)處理后iOS也可提升10%以上的秒開。

安卓和iOS實(shí)現(xiàn)差異
????????經(jīng)過分析對(duì)比發(fā)現(xiàn),安卓的攔截動(dòng)作比較輕,可以判斷是否需要攔截,不需要攔截可以交給WebView自己去請(qǐng)求。
????????但是iOS這邊一旦頁(yè)面開啟攔截后,頁(yè)面中所有的http和https請(qǐng)求都會(huì)被攔截掉,由客戶端發(fā)起請(qǐng)求進(jìn)行響應(yīng),無法將請(qǐng)求交還給WebView自己去發(fā)起。
iOS 緩存問題修復(fù)
????????頁(yè)面中的資源經(jīng)過客戶端請(qǐng)求代理后原本第二次打開WebView本身會(huì)使用緩存的內(nèi)存,現(xiàn)在緩存也失效了,于是只能在客戶端內(nèi)實(shí)現(xiàn)了一套緩存機(jī)制。
根據(jù) http 協(xié)議來判定哪些資源可以被緩存以及緩存的時(shí)長(zhǎng)
添加自定義的控制策略,僅允許部分類型的資源進(jìn)行緩存
3.3.4 離線包下載錯(cuò)誤率治理
????????從下圖可以看到離線包的下載錯(cuò)誤率在 6% 左右浮動(dòng),這么高的下載錯(cuò)誤率肯定是無法接受的, 經(jīng)過一系列優(yōu)化手段,把離線包下載錯(cuò)誤率從6%左右浮動(dòng)下降至 0.3%左右浮動(dòng)。

先來看下優(yōu)化前的流程圖和問題點(diǎn)。

????????通過埋點(diǎn)發(fā)現(xiàn)大量 unknown host 、網(wǎng)絡(luò)請(qǐng)求失敗、網(wǎng)絡(luò)連接斷開的情況。分析代碼發(fā)現(xiàn)下載未做隊(duì)列控制,會(huì)同時(shí)并發(fā)下載多個(gè)離線包,從而導(dǎo)致多個(gè)下載任務(wù)爭(zhēng)搶資源的情況。針對(duì)發(fā)現(xiàn)的問題點(diǎn)做出了以下優(yōu)化:
下載失敗添加重試機(jī)制,并可動(dòng)態(tài)配置重試次數(shù)用于緩解網(wǎng)絡(luò)請(qǐng)求失敗、網(wǎng)絡(luò)連接斷開的問題。
添加下載任務(wù)隊(duì)列管理功能,可動(dòng)態(tài)配置并發(fā)下載數(shù)量,用于緩解不同下載任務(wù)爭(zhēng)搶資源的問題。
針對(duì)弱網(wǎng)和無網(wǎng)絡(luò)情況延遲到網(wǎng)絡(luò)良好時(shí)下載。
離線包下載支持 httpdns,用于解決域名無法解析的情況。
下面是優(yōu)化之后的流程圖:

3.3.5 展望
????????針對(duì)離線資源是直接存儲(chǔ)在磁盤上的,每次訪問都會(huì)有磁盤IO耗時(shí),經(jīng)過在低端機(jī)器上做測(cè)試發(fā)現(xiàn)這個(gè)耗時(shí)會(huì)在 0 - 10ms 之間進(jìn)行波動(dòng),后面會(huì)把內(nèi)存合理的利用起來,通過設(shè)置內(nèi)存上限,文件數(shù)量上限,甚至是文件類型,并通過 LRU 策略進(jìn)行內(nèi)存文件的淘汰更新。
3.4 接口預(yù)請(qǐng)求
????????通過客戶端發(fā)起H5頁(yè)面首屏接口請(qǐng)求,遠(yuǎn)比等待客戶端頁(yè)面初始化、下載HTML、JS下載執(zhí)行的時(shí)機(jī)更提前,從而節(jié)省用戶的首屏等待時(shí)間。在本地測(cè)試過程中發(fā)現(xiàn)接口預(yù)請(qǐng)求可提前100+ms,用戶也就可以更快的看到內(nèi)容。

3.4.1 功能介紹
????????客戶端會(huì)在App啟動(dòng)后獲取配置,保存支持預(yù)請(qǐng)求的頁(yè)面地址及對(duì)應(yīng)的接口信息,在用戶打開WebView時(shí),會(huì)并行發(fā)起對(duì)應(yīng)預(yù)請(qǐng)求的接口,并保存結(jié)果。當(dāng)JS執(zhí)行開始獲取首屏數(shù)據(jù)時(shí),會(huì)先詢問客戶端是否已經(jīng)存有對(duì)應(yīng)的響應(yīng)數(shù)據(jù),如果此時(shí)已經(jīng)拿到數(shù)據(jù)則無需發(fā)起請(qǐng)求,否則 js 也會(huì)發(fā)起接口請(qǐng)求并開啟競(jìng)速模式。以下是整體流程圖:

3.4.2 配置平臺(tái)
????????那么客戶端怎么知道這個(gè)頁(yè)面需要請(qǐng)求什么接口呢?以及接口的參數(shù)是什么呢?那自然少不了配置平臺(tái),它支持以下功能:
配置需要預(yù)加載的頁(yè)面 url并對(duì)應(yīng)一個(gè)需要請(qǐng)求的 api url 以及參數(shù)
配置審核功能,避免錯(cuò)誤配置發(fā)布上線
3.4.3 QA?既然有了SSR服務(wù)端渲染為什么還需要接口預(yù)請(qǐng)求的功能?
????????首先即使是在 SSR 的情況下,首屏內(nèi)容中仍然可能有部分組件是骨架直出的,需要等待頁(yè)面渲染執(zhí)行時(shí)才會(huì)去請(qǐng)求數(shù)據(jù),另外還有一部分頁(yè)面是SPA的。針對(duì)這兩種情況都能做到很好的補(bǔ)充。
3.5 預(yù)建連&鏈接?;?/span>
????????開啟后DNS 90分位耗時(shí)從80ms降至0ms,TCP建連90分位耗時(shí)從65ms分位耗時(shí)降為0,DNS平均耗時(shí)從55ms降為4.3ms,TCP建連平均耗時(shí)從30ms降為2.5ms。
3.5.1 網(wǎng)絡(luò)請(qǐng)求耗時(shí)分析

????????通過上圖可以看到一個(gè)網(wǎng)絡(luò)請(qǐng)求在經(jīng)過DNS解析耗時(shí)、TCP建連耗時(shí)、SSL建連耗時(shí)階段之后才能把請(qǐng)求發(fā)出去,那么是否可以節(jié)省這段時(shí)間的耗時(shí)呢?
????????客戶端常用的網(wǎng)絡(luò)請(qǐng)求框架如OkHttp等,都能完整支持http1.1與HTTP2的功能,也就支持連接復(fù)用。了解了這個(gè)連接復(fù)用機(jī)制優(yōu)勢(shì),那就可以利用起來,比如在APP開屏等待的時(shí)候,就預(yù)先建立關(guān)鍵域名的連接,這樣進(jìn)入相應(yīng)頁(yè)面后可以更快的獲取到網(wǎng)絡(luò)請(qǐng)求結(jié)果,給予用戶更好體驗(yàn)。在網(wǎng)絡(luò)環(huán)境偏差的情況下,這種預(yù)連接理論上會(huì)有更好的效果。
3.5.2 實(shí)現(xiàn)方案
????????可以通過對(duì)域名鏈接提前發(fā)起一個(gè)HEAD請(qǐng)求從而建立鏈接,網(wǎng)絡(luò)框架會(huì)自動(dòng)將連接放入連接池。并在默認(rèn)無操作5分鐘后進(jìn)行釋放,在五分鐘內(nèi)重復(fù)執(zhí)行上述動(dòng)作即可一直保持鏈接。
????????另外這里需要注意下連接池的數(shù)量問題,如果連接池的數(shù)據(jù)太小,但是域名比較多的話,通過預(yù)建連保持的鏈接很容易就會(huì)被釋放掉,這就需要通過域名收斂或者調(diào)大連接池的數(shù)量來進(jìn)行優(yōu)化。
3.5.3 線上收益
????????那預(yù)建連會(huì)不會(huì)增加服務(wù)器的壓力呢?這個(gè)肯定是會(huì)的,首先會(huì)針對(duì)預(yù)建連功能本身就行灰度策略,在HTML頁(yè)面通過CDN托管后,直接針對(duì) cdn 域名進(jìn)行全量開啟,從而不用擔(dān)心 cdn 域名扛不住壓力。
????????來看一下線上效果,通過下圖可以看到在開啟后DNS 90分位耗時(shí)從80ms降至0ms,TCP建連90分位耗時(shí)從65ms分位耗時(shí)降為0,DNS平均耗時(shí)從55ms降為4.3ms,TCP建連平均耗時(shí)從30ms降為2.5ms。

3.6 預(yù)渲染
????????客戶端提前通過WebView將頁(yè)面渲染好,等待用戶訪問時(shí),可直接展示。從而達(dá)到瞬開效果。但是這種功能肯定不能對(duì)所有的頁(yè)面進(jìn)行開放,而且存在一定的弊端。
會(huì)額外消耗客戶端資源,需要在主線程空閑時(shí)執(zhí)行,并需要控制預(yù)渲染的頁(yè)面數(shù)量。
如果頁(yè)面一進(jìn)入就會(huì)下紅包雨,這種頁(yè)面是不適合做預(yù)渲染的,需要進(jìn)行規(guī)避。
????????下圖【開學(xué)季】是業(yè)務(wù)上已經(jīng)進(jìn)行預(yù)渲染的H5頁(yè)面,可以看到在打開【開學(xué)季】頁(yè)面時(shí),頁(yè)面已經(jīng)渲染完畢,絲毫沒有等待過程。

后面計(jì)劃把這種能力放大到通用的webview上,針對(duì)大促以及PV量高的頁(yè)面進(jìn)行開放。
4. H5 優(yōu)化
4.1 SSR服務(wù)端渲染
????????SSR服務(wù)端渲染(英語:server side render)一般情況下,一個(gè)H5頁(yè)面的數(shù)據(jù)渲染完全由客戶端來完成,先通過AJAX請(qǐng)求到頁(yè)面數(shù)據(jù)并把相應(yīng)的數(shù)據(jù)填充到模板,形成完整的頁(yè)面來呈現(xiàn)給用戶。而服務(wù)端渲染把數(shù)據(jù)的請(qǐng)求和模板的填充放在了服務(wù)端,并把渲染的完整的頁(yè)面返回給客戶端。

??????? SSR對(duì)于秒開有平均15%的提升,既然是服務(wù)端渲染,就會(huì)對(duì)服務(wù)器造成壓力,尤其是在預(yù)加載HTML功能開啟后,那得物是如何解決的呢?
4.1.1 前期優(yōu)化內(nèi)容
接口緩存:node服務(wù)向ctx中注入redis實(shí)例,業(yè)務(wù)方在服務(wù)端渲染的邏輯中處理接口相關(guān)的緩存,這其中涉及:配置文件下發(fā)、ab接口。
靜態(tài)頁(yè)面緩存:由于頁(yè)面完成是無接口交互,并且所有用戶展示都是一樣的,由renderToHtml生成靜態(tài)html資源,寫入緩存。
無用戶狀態(tài)頁(yè)面緩存:此類頁(yè)面大多數(shù)情況下展示內(nèi)容都是一樣的,服務(wù)端請(qǐng)求的數(shù)據(jù)也都是一致的,服務(wù)端處理的時(shí)候根據(jù)是否有用戶登陸狀態(tài)判斷是否需要緩存。
千人千面接口內(nèi)容由SSR修改為CSR,并顯示骨架圖,由于千人千面內(nèi)容都是由算法接口提供,且算法接口本身響應(yīng)較慢,因此通過這種方式可以減少服務(wù)端響應(yīng)耗時(shí),且能更快速的給用戶展示內(nèi)容。
4.1.2 破局者CDN
????????通過這么多優(yōu)化手段仍然無法滿足預(yù)加載的需求,并且通過分析發(fā)現(xiàn)網(wǎng)絡(luò)階段耗時(shí)較長(zhǎng),最終還是搬出了CDN這個(gè)大殺器,一直沒上CDN的原因有很多,主要有以下幾方面:
(1)得物的頁(yè)面是千人千面的每個(gè)人看到的內(nèi)容都不同
????????通過上述優(yōu)化4即可解決,將原本SSR渲染的內(nèi)容修改CSR,由于已經(jīng)上了CDN了,后續(xù)計(jì)劃將這部分內(nèi)容再次修改回SSR,這樣用戶可以更快的看到商品而不是骨架,然后通過 CSR 的方式來更新內(nèi)容。
(2)頁(yè)面狀態(tài)變更,無法及時(shí)更新緩存
????????這個(gè)問題在上述客戶端預(yù)加載優(yōu)化部分已經(jīng)有解決方案了,可以通過在頁(yè)面打開后針對(duì)有需要的組件再次請(qǐng)求接口刷新數(shù)據(jù)以確保數(shù)據(jù)的準(zhǔn)確性,但是這部分工作量也是比較大的,梳理出來的需要刷新狀態(tài)的組件就有30+,而且之后開發(fā)的組件都需要考慮狀態(tài)更新的事情。
(3)HTML模板內(nèi)容變更無法及時(shí)更新
????????引起模板內(nèi)容變更的地方有兩處,第一個(gè)場(chǎng)景就是在搭建器場(chǎng)景下,運(yùn)營(yíng)可以動(dòng)態(tài)修改模板內(nèi)容導(dǎo)致頁(yè)面結(jié)構(gòu)變更(低頻次),第二個(gè)場(chǎng)景是項(xiàng)目發(fā)版后模板內(nèi)容需要更新(高頻)。
????????這個(gè)問題可以通過在感知到內(nèi)容變更時(shí)自動(dòng)調(diào)用CDN服務(wù)商的刷新緩存接口來更新CDN緩存內(nèi)容。
4.2 預(yù)渲染HTML
????????通過puppeteer將SPA頁(yè)面渲染出來并將HTML文檔進(jìn)行保存,配合上述頁(yè)面刷新策略,并將HTML通過CDN進(jìn)行托管,讓你的 SPA 頁(yè)面 像 SSR 一樣絲滑。
????????主要實(shí)現(xiàn)方案是通過基于webpack的插件prerender-spa-plugin,并配置需要預(yù)渲染的路由,這樣經(jīng)過打包之后就會(huì)產(chǎn)出對(duì)應(yīng)路由的頁(yè)面。方案本身是通用的,但是每接入一個(gè)頁(yè)面都需要人工check。
4.3 不起眼的css包大小優(yōu)化
????????眾所周知 css 加載會(huì)阻塞HTML渲染,最終將首屏公共css從118kb縮減至38kb,下圖通過 chrome 模擬弱網(wǎng)環(huán)境下的SSR頁(yè)面加載時(shí)序圖。從圖中可以看出 styles.fb201fce.chunk.css 下載耗時(shí) 18s,阻塞了頁(yè)面的渲染,HTML 主文檔耗時(shí) 2.38s 就已經(jīng)下載完成了,但是實(shí)際渲染時(shí)間卻是在 20s 之后。

????????優(yōu)化思路也比較單間,將首屏所需要的css 文件通過內(nèi)聯(lián)方式內(nèi)嵌到HTML中,由SSR服務(wù)一并返回,并對(duì)css文件進(jìn)行拆分,按需加載。
????????思路有了,接下來就看怎么去實(shí)現(xiàn)了,最初嘗試了MiniCssExtractPlugin 插件他可以把css分成單獨(dú)的文件,并且每一個(gè)js會(huì)對(duì)應(yīng)生成一個(gè)css文件,但是他需要建立在webpack5之上,然而項(xiàng)目中使用的next版本是9.5,于是就想著升級(jí)到最新版next12,升級(jí)后發(fā)現(xiàn),在構(gòu)建中其他包各種報(bào)錯(cuò),發(fā)現(xiàn)有些包并不支持最新的next12,在嘗試一天的修復(fù)之后,仍未解決,且升級(jí)到最新版不確定是否會(huì)引發(fā)其他穩(wěn)定性問題,暫且擱置尋找其他方法。
????????經(jīng)過不懈的努力,通過閱讀 next 源碼發(fā)現(xiàn)了端倪,發(fā)現(xiàn)在打包時(shí)將所有的公共css通過 splitChunks 進(jìn)行分組,由于項(xiàng)目中組件都是動(dòng)態(tài)引入的,這里直接在 next.config.js 中修改webpack打包參數(shù),將 splitChunks.cacheGroups.styles 配置刪除,使用默認(rèn)的 chunks: async 配置,即可實(shí)現(xiàn)按需引入。
4.4 圖片優(yōu)化
(1)避免圖片src為空
????????雖然src屬性為空字符串,但瀏覽器仍然會(huì)向服務(wù)器發(fā)起一個(gè)HTTP請(qǐng)求,尤其是在SSR服務(wù)器壓力扛不住的情況下,因此這里需要特別注意一下。
(2)圖片壓縮以及格式選擇
??????? WebP 的優(yōu)勢(shì)體現(xiàn)在它具有更優(yōu)的圖像數(shù)據(jù)壓縮算法,能帶來更小的圖片體積,而且擁有肉眼識(shí)別無差異的圖像質(zhì)量;同時(shí)具備了無損和有損的壓縮模式、Alpha 透明以及動(dòng)畫的特性,在 JPEG 和 PNG 上的轉(zhuǎn)化效果都相當(dāng)優(yōu)秀、穩(wěn)定和統(tǒng)一。
通過向圖片服務(wù)器傳遞參數(shù)選擇合適的分辨率。
4.5 細(xì)節(jié)優(yōu)化
(1)打包優(yōu)化
頁(yè)面組件拆分,優(yōu)先加載首屏內(nèi)容所需資源
webpack splitchunks 有效拆分公共依賴,提高緩存利用率
組件按需加載
Tree Shaking 縮減代碼體積
(2)非關(guān)鍵js、css延遲加載
defer、async、動(dòng)態(tài)加載js
iOS 設(shè)備延遲加載 js
(3)媒體資源加載優(yōu)化
圖片、視頻懶加載
資源壓縮、通過向圖片服務(wù)器傳遞參數(shù)選擇合適的分辨率
(4)其他資源優(yōu)化
數(shù)據(jù)埋點(diǎn)上報(bào)延遲發(fā)送,不阻塞 onload 事件觸發(fā)
自定義字體優(yōu)化,使用 fontmin 生成精簡(jiǎn)的字體包
(5)頁(yè)面渲染優(yōu)化
頁(yè)面渲染時(shí)間優(yōu)化
SSR頁(yè)面首屏 css 內(nèi)聯(lián)(Critial CSS)
合理使用 Layers
布局抖動(dòng)優(yōu)化:提前定好寬高
減少重排重繪操作
(6)代碼層面優(yōu)化
耗時(shí)任務(wù)分割
通過 Web Worker 減少主線程耗時(shí)
通過 RAF 回調(diào),在線程空閑時(shí)執(zhí)行代碼邏輯
避免 css 嵌套過深
5. 監(jiān)控
?????為了幫助開發(fā)者更好地衡量和改進(jìn)前端頁(yè)面性能,W3C性能小組引入了 Performance API ,其中Navigation Timing API 實(shí)現(xiàn)了自動(dòng)、精準(zhǔn)的頁(yè)面性能打點(diǎn)。得物前端性能監(jiān)控指標(biāo)也都是從 Performance API 中獲取數(shù)據(jù)進(jìn)行上報(bào)統(tǒng)計(jì)分析的。

5.1 系統(tǒng)架構(gòu)
????????SDK 數(shù)據(jù)采集完畢后,會(huì)上報(bào)到 阿里云 sls 日志平臺(tái),隨后通過 flink 實(shí)時(shí)消費(fèi)清洗數(shù)據(jù)后落庫(kù)至 clickhouse 中,平臺(tái)后端通過讀取 clickhouse 數(shù)據(jù)并做各種聚合處理后使用。

5.2 指標(biāo)大盤
????????做優(yōu)化之前首先要建立監(jiān)控指標(biāo),互聯(lián)網(wǎng)稱之為抓手,沒有監(jiān)控指標(biāo)的情況下,任你怎么優(yōu)化,都不知道優(yōu)化的效果怎么樣,更不知道下一步該做什么?以及還有哪些問題沒解決。因此優(yōu)化之前指標(biāo)先行,當(dāng)然一定要指標(biāo)的準(zhǔn)確性。
指標(biāo)大盤主要包含以下功能:
可快速查看某段時(shí)間內(nèi)的版本、設(shè)備廠商、設(shè)備名、設(shè)備系統(tǒng)版本以及網(wǎng)絡(luò)占比情況,還可以根據(jù)這些字段進(jìn)行篩選排查。
中間區(qū)域展示了比較關(guān)注的整體以及活動(dòng)頁(yè)面的客戶端耗時(shí)和H5秒開耗時(shí)的情況。
底部區(qū)域展示了各業(yè)務(wù)域的秒開耗時(shí)情況。
這里同時(shí)展示了平均耗時(shí)和90分位耗時(shí),平均耗時(shí)有個(gè)弊端就是很容易被平均,大家應(yīng)該都有被平均的經(jīng)歷。90分位耗時(shí)這里簡(jiǎn)單講解一下:意思就是有90%的訪問耗時(shí)是低于90分位耗時(shí)的,以此類推 50 分位就是有50%的訪問耗時(shí)是低于50分位的,分位值是將所有的耗時(shí)數(shù)據(jù)從小到大排序后得出的。
5.3 白屏監(jiān)控

????????在正常情況下,完成上述的優(yōu)化措施后用戶基本是可以秒開 H5 頁(yè)面的了。但異常情況總是會(huì)有的,用戶的網(wǎng)絡(luò)環(huán)境和系統(tǒng)環(huán)境千差萬別,甚至 WebView 也可能發(fā)生內(nèi)部崩潰。當(dāng)發(fā)生問題時(shí),用戶看到的可能就直接只是一個(gè)白屏頁(yè)面了,所以進(jìn)一步的優(yōu)化手段就是需要去檢測(cè)是否發(fā)生白屏以及相應(yīng)的應(yīng)對(duì)措施。
????????檢測(cè)白屏最直觀的方案就是對(duì) WebView 進(jìn)行截圖,遍歷截圖的像素點(diǎn)的顏色值,如果非純色的顏色點(diǎn)超過一定的閾值,就可以認(rèn)為不是白屏。首先獲取包含 WebView 視圖的 Bitmap 對(duì)象,然后把截圖縮小到指定的分辨率大小如:100*auto,遍歷檢測(cè)圖片的像素點(diǎn),當(dāng)非純色的像素點(diǎn)大于 5% 的時(shí)候就可以認(rèn)為是非白屏的情況,但是還有很多列外的情況,我們通過圖片識(shí)別技術(shù)對(duì)截圖進(jìn)行分析,可以很好的感知當(dāng)前是否白屏、是不是在loading、是不是特殊頁(yè)面等。
????????白屏是一個(gè)重要的指標(biāo),我們針對(duì)整體白屏率快速拉升、新增白屏頁(yè)面發(fā)出告警通知,便于開發(fā)人員及時(shí)介入開始排查問題。
5.4 性能問題發(fā)現(xiàn)
????????主要通過 CDN 未覆蓋監(jiān)控、http請(qǐng)求監(jiān)控、網(wǎng)絡(luò)監(jiān)控(加載失敗、耗時(shí)異常、傳輸大小異常)、圖片監(jiān)控(未壓縮、分辨率異常)等監(jiān)控手段發(fā)現(xiàn)頁(yè)面中的潛在問題,同時(shí)還提供了問題分析能力,在問題分析頁(yè)面輸入頁(yè)面url地址即可幫助您發(fā)現(xiàn)問題并給出修改建議。
5.4.1 CDN 未覆蓋監(jiān)控
????
??????? CDN的重要性不言而喻,它可以加速資源訪問速度,從而提升用戶體驗(yàn),我們通過對(duì)線上埋點(diǎn)數(shù)據(jù)分析,找出CDN未覆蓋的資源列表,從而推動(dòng)各業(yè)務(wù)同學(xué)優(yōu)化。
5.4.2 HTTP請(qǐng)求監(jiān)控
為什么要監(jiān)控HTTP請(qǐng)求呢?我們先來看一下HTTPS相對(duì)于HTTP新增的特點(diǎn):
內(nèi)容加密:采用混合加密技術(shù),中間者無法直接查看明文內(nèi)容
驗(yàn)證身份:通過證書認(rèn)證客戶端訪問的是自己的服務(wù)器
保護(hù)數(shù)據(jù)完整性:防止傳輸?shù)膬?nèi)容被中間人冒充或者篡改
????????那么HTTP就容易被中間人查看到內(nèi)容,甚至被篡改,既然如此為了我們服務(wù)的安全性就需要對(duì)現(xiàn)有的http協(xié)議統(tǒng)一進(jìn)行升級(jí)改造,那就需要監(jiān)控去發(fā)現(xiàn)。
5.4.3 網(wǎng)絡(luò)監(jiān)控
????????某些頁(yè)面秒開率低,那就要分析一下原因,是不是這個(gè)頁(yè)面的接口響應(yīng)比較慢呢,還是說頁(yè)面本身有請(qǐng)求比較大的資源?如果發(fā)生網(wǎng)絡(luò)請(qǐng)求失敗的情況也要第一時(shí)間感知,不能被動(dòng)等待用戶反饋。
5.4.4 圖片監(jiān)控
????????包含圖片未壓縮、圖片分辨率異常、圖片傳輸大小大于 300kb 異常、動(dòng)圖資源傳輸大小大于 1M 異常功能。
5.4.5 頁(yè)面問題分析
????????上面列出來一堆功能,對(duì)于業(yè)務(wù)的同學(xué)可能比較煩惱,我一個(gè)頁(yè)面具體有哪些問題呢?你不能讓我去上面的功能里面一個(gè)個(gè)看,哪個(gè)異常是我負(fù)責(zé)的頁(yè)面的吧?這個(gè)功能本身就行將現(xiàn)有的功能利用起來,通過一個(gè)頁(yè)面path進(jìn)行聚合分析。

5.5 異常監(jiān)控
??????? H5異常一直是使用 sentry 進(jìn)行監(jiān)控的,但是sentry系統(tǒng)因缺乏同PV、DAU數(shù)據(jù)的關(guān)聯(lián)性,因此無法衡量產(chǎn)品異常發(fā)生后所帶來的嚴(yán)重程度。在業(yè)務(wù)域關(guān)聯(lián)上的缺失導(dǎo)致異常問題無法根據(jù)業(yè)務(wù)域進(jìn)行劃分。用戶行為日志也尚未與Native 端側(cè)打通,在問題分析時(shí)容易遇到上下文不全的瓶頸。還有一個(gè)問題是sentry會(huì)有限流措施,當(dāng)qps較高時(shí)會(huì)丟棄一部分異常數(shù)據(jù)。

????????由于sentry已經(jīng)可以幫助我們進(jìn)行一定的問題排查分析能力,我們不打算做sentry同樣的功能,而是做sentry不支持的部分,針對(duì)上述問題我們?cè)O(shè)計(jì)了以下功能:
異常問題指標(biāo)衡量
增加異常率、頁(yè)面異常率、影響用戶率趨勢(shì)
增加問題多維度(系統(tǒng)版本、APP版本、H5發(fā)布版本、網(wǎng)絡(luò)等)下的分布占比、業(yè)務(wù)域劃分
異常問題聚合能力增強(qiáng)(聚合問題能力增強(qiáng))
異常列表支持最新新增、Top PV、異常次數(shù)、影響用戶數(shù)排序
區(qū)分三方sdk異常、接口異常等不同異常類型劃分
6. 未來展望&總結(jié)
????????雖然目前秒開率已經(jīng)做到了75%以上,但是同時(shí)我們還有一個(gè)重要的指標(biāo),90分位耗時(shí),致力于提升末尾用戶H5頁(yè)面使用體驗(yàn),在90分位優(yōu)化完成后,可能會(huì)考慮繼續(xù)深入優(yōu)化95分位耗時(shí)。
????????最后感謝那些為得物H5頁(yè)面秒開做出貢獻(xiàn)的同學(xué),感謝H5團(tuán)隊(duì),同學(xué)們都很棒,各種優(yōu)化手段和想法層出不窮。
????????至此我們系統(tǒng)的講解了背景以及從指標(biāo)建立到秒開優(yōu)化上線的全過程,全文分成了三個(gè)部分,客戶端、H5、以及監(jiān)控。如果閱讀本文對(duì)您有所收獲,麻煩您動(dòng)一動(dòng)發(fā)財(cái)?shù)男∈贮c(diǎn)個(gè)贊吧!如果閱讀完還意猶未盡或者有什么問題和想法歡迎留言區(qū)評(píng)論交流。
最后奉上整體優(yōu)化腦圖:

Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對(duì)你有幫助,我想請(qǐng)你幫我2個(gè)小忙:
1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客?www.inode.club?讓我們一起成長(zhǎng) 點(diǎn)贊和在看就是最大的支持??
