1. 程式人生 > 其它 >WebView效能、體驗分析與優化

WebView效能、體驗分析與優化

在App開發中,內嵌WebView始終佔有著一席之地。它能以較低的成本實現Android、iOS和Web的複用,也可以冠冕堂皇的突破蘋果對熱更新的封鎖。

然而便利性的同時,WebView的效能體驗卻備受質疑,導致很多客戶端中需要動態更新等頁面時不得不採用其他方案。

以發展的眼光來看,功能的動態載入以及三端的融合將會是大趨勢。那麼如何克服WebView固有的問題呢?我們將從效能、記憶體消耗、體驗、安全幾個維度,來系統的分析客戶端預設WebView的問題,以及對應的優化方案。

效能

對於WebView的效能,給人最直觀的莫過於:開啟速度比native慢。

是的,當我們開啟一個WebView頁面,頁面往往會慢吞吞的loading很久,若干秒後才出現你所需要看到的頁面。

這是為什麼呢?

對於一個普通使用者來講,開啟一個WebView通常會經歷以下幾個階段:

  1. 互動無反饋
  2. 到達新的頁面,頁面白屏
  3. 頁面基本框架出現,但是沒有資料;頁面處於loading狀態
  4. 出現所需的資料

如果從程式上觀察,WebView啟動過程大概分為以下幾個階段:

如何縮短這些過程的時間,就成了優化WebView效能的關鍵。

接下來我們逐一分析各個階段的耗時情況,以及需要注意的優化點。

WebView初始化

當App首次開啟時,預設是並不初始化瀏覽器核心的;只有當建立WebView例項的時候,才會建立WebView的基礎框架。

所以與瀏覽器不同,App中開啟WebView的第一步並不是建立連線,而是啟動瀏覽器核心。

我們來分析一下這段耗時到底需要多久。

分析

針對WebView的初始化時間,我們可以定義兩個指標:

  • 首次初始化時間:客戶端冷啟動後,第一次開啟WebView,從開始建立WebView到開始建立網路連線之間的時間。
  • 二次初始化時間:在開啟過WebView後,退出WebView,再重新開啟WebView,從開始建立WebView到開始建立網路連線之間的時間。

測試資料:

測試系統1: iOS模擬器,Titans 10.0.7 測試系統2: OPPO R829T Android 4.2.2 測試方式:測試10次取平均值 測試App:美團外賣 單位:ms

首次初始化時間

二次初始化時間

iOS(UIWebView)

306.56

76.43

iOS(WKWebView)

763.26

457.25

Android

192.79 *

142.53

* Android外賣客戶端啟動後會在後臺開啟WebView程序,故並不是完全新建WebView時間。

這意味著什麼呢?

作為前端工程師,統計了無數次的頁面開啟時間,都是以網路連線開始作為起點的。

很遺憾的通知您:WebView中使用者體驗到的開啟時間需要再增加70~700ms。

於是我們找到了“為什麼WebView總是很慢”的原因之一:

  • 在瀏覽器中,我們輸入地址時(甚至在之前),瀏覽器就可以開始載入頁面。
  • 而在客戶端中,客戶端需要先花費時間初始化WebView完成後,才開始載入。

而這段時間,由於WebView還不存在,所有後續的過程是完全阻塞的。可以這樣形容WebView初始化過程:

那麼有哪些解決辦法呢?

怎麼優化

由於這段過程發生在native的程式碼中,單純靠前端程式碼是無法優化的;大部分的方案都是前端和客戶端協作完成,以下是幾個業界採用過的方案。

全域性WebView

方法:

  • 在客戶端剛啟動時,就初始化一個全域性的WebView待用,並隱藏;
  • 當用戶訪問了WebView時,直接使用這個WebView載入對應網頁,並展示。

這種方法可以比較有效的減少WebView在App中的首次開啟時間。當用戶訪問頁面時,不需要初始化WebView的時間。

當然這也帶來了一些問題,包括:

  • 額外的記憶體消耗。
  • 頁面間跳轉需要清空上一個頁面的痕跡,更容易記憶體洩露。

【參考東軟專利 - 載入網頁的方法及裝置 CN106250434A

客戶端代理資料請求

方法:

  • 在客戶端初始化WebView的同時,直接由native開始網路請求資料;
  • 當頁面初始化完成後,向native獲取其代理請求的資料。

此方法雖然不能減小WebView初始化時間,但資料請求和WebView初始化可以並行進行,總體的頁面載入時間就縮短了;縮短總體的頁面載入時間:

【參考騰訊分享:70%以上業務由H5開發,手機QQ Hybrid 的架構如何優化演進?

還有其他各種優化的方式,不再一一列舉,總結起來都是圍繞兩點:

  1. 在使用前預先初始化好WebView,從而減小耗時。
  2. 在初始化的同時,通過Native來完成一些網路請求等過程,使得WebView初始化不是完全的阻塞後續過程。

建立連線/伺服器處理

在頁面請求的資料返回之前,主要有以下過程耗費時間。

  • DNS
  • connection
  • 伺服器處理

分析

以下為美團中活動頁面的連結時間統計:

統計: 美團的活動頁面 內容值: n%分位值(ms)

DNS

connection

獲取首位元組

50%

1.3

71

172

90%

60

360

541

優化

這些時間都是發生在網頁載入之前,但這並不意味著無法優化,有以下幾種方法。

DNS採用和客戶端API相同的域名

DNS會在系統級別進行快取,對於WebView的地址,如果使用的域名與native的API相同,則可以直接使用快取的DNS而不用再發起請求圖片。

以美團為例,美團的客戶端請求域名主要位於api.meituan.com,然而內嵌的WebView主要位於 i.meituan.com。

當我們初次開啟App時:

  • 客戶端首次開啟都會請求api.meituan.com,其DNS將會被系統快取。
  • 然而當開啟WebView的時候,由於請求了不同的域名,需要重新獲取i.meituan.com的IP。

根據上面的統計,至少10%的使用者開啟WebView時耗費了60ms在DNS上面,如果WebView的域名與App的API域名統一,則可以讓WebView的DNS時間全部達到1.3ms的量級。

靜態資源同理,最好與客戶端的資源域名保持一致。

同步渲染採用chunk編碼

同步渲染時如果後端請求時間過長,可以考慮採用chunk編碼,將資料放在最後,並優先將靜態內容flush。對於傳統的後端渲染頁面,往往都是使用的【瀏覽器】--> 【Web API】 --> 【業務 API】的載入模式,其中後端時間就指的是Web API的處理時間了。

在這裡Web API一般有兩個作用:

  1. 確定靜態資源的版本。
  2. 根據使用者的請求,去業務API獲取資料。

而一般確定靜態資源的版本往往是直接讀取程式碼版本,基本無耗時;而主要的後端時間都花費在了業務API請求上面。

那麼怎麼優化利用這段時間呢?

在HTTP協議中,我們可以在header中設定 transfer-encoding:chunked使得頁面可以分塊輸出。如果合理設計頁面,讓head部分都是確定的靜態資源版本相關內容,而body部分是業務資料相關內容,那麼我們可以在使用者請求的時候,首先將Web API可以確定的部分先輸出給瀏覽器,然後等API完全獲取後,再將API資料傳輸給瀏覽器。

下圖可以直觀的看出分chunk輸出和一起輸出的區別:

  • 如果採用普通方式輸出頁面,則頁面會在伺服器請求完所有API並處理完成後開始傳輸。瀏覽器要在後端所有API都載入完成後才能開始解析。
  • 如果採用chunk-encoding: chunked,並優先將頁面的靜態部分輸出;然後處理API請求,並最終返回頁面,可以讓後端的API請求和前端的資源載入同時進行。
  • 兩者的總共後端時間並沒有區別,但是可以提升首位元組速度,從而讓前端載入資源和後端載入API不互相阻塞。

頁面框架渲染

頁面在解析到足夠多的節點,且所有CSS都載入完成後進行首屏渲染。在此之前,頁面保持白屏;在頁面完全下載並解析完成之前,頁面處於不完整展示狀態。

分析

我們以一個美團的活動頁面作為樣例:

測試頁面:http://i.meituan.com/firework/meituanxianshifengqiang 在Mac上面,模擬4G情況 頁面樣式:

測試得到的時間耗費如下:

表1

階段

時間

大小

備註

DOM下載

58ms

29.5 KB

4G網路

DOM解析

12.5ms

198 KB

根據估算,在手機上慢2~5倍不等

CSS請求+下載

58ms

11.7 KB

4G網路(包含連結時間,CDN)

CSS解析

2.89ms

54.1 KB

根據估算,在手機上慢2~5倍不等

渲染

23ms

1361節點

根據估算,在手機上慢2~5倍不等

繪製

4.1ms

根據估算,在手機上慢2~5倍不等

合成

0.23ms

GPU處理

同時,對HTML的載入時間進行分析,可以得到如下時間點。

表2

指標

時間

計算方法

HTML載入完成時間

218

performance.timing.responseEnd - performance.timing.fetchStart

HTML解析完成時間

330

performance.timing.domInteractive - performance.timing.fetchStart

這意味著什麼呢?

對於表1

可以看到,隨著在網路優良的情況下,Dom的解析所佔耗時比例還是不算低的,對於低端機器更甚。Layout時間也是首屏前耗時的大頭,據猜測這與頁面使用了rem作為單位有關(待進一步分析)。

對於表2,我們可以發現一個問題

一般來說HTML在開始接收到返回資料的時候就開始解析HTML並構建DOM樹。如果沒有JS(JavaScript)阻塞的話一般會相繼完成。然而,在這裡時間相差了90ms……也就是說,解析被阻塞了。

進一步分析可以發現,頁面的header部分有這樣的程式碼:

.....
<link href="//ms0.meituan.net/css/eve.9d9eee71.css" rel="stylesheet" onload="MT.pageData.eveTime=Date.now()"/>
<script>
window.fk = function (callback) {
require(['util/native/risk.js'], function (risk) {
    risk.getFk(callback);
});
}
</script>
</head>
....

通常情況下,上面程式碼的link部分和script部分如果單獨出現,都不會阻塞頁面的解析:

  • CSS不會阻止頁面繼續向下繼續。
  • 內聯的JS很快執行完成,然後繼續解析文件。

然而,當這兩部分同時出現的時候,問題就來了。

  • CSS載入阻塞了下面的一段內聯JS的執行,而被阻塞的內聯JS則阻塞了HTML的解析。

通常情況下,CSS不會阻塞HTML的解析,但如果CSS後面有JS,則會阻塞JS的執行直到CSS載入完成(即便JS是內聯的指令碼),從而間接阻塞HTML的解析。

優化

在頁面框架載入這一部分,能夠優化的點參照雅虎14條就夠了;但注意不要犯錯,一個小小的內聯JS放錯位置也會讓效能下降很多。

  1. CSS的載入會在HTML解析到CSS的標籤時開始,所以CSS的標籤要儘量靠前。
  2. 但是,CSS連結下面不能有任何的JS標籤(包括很簡單的內聯JS),否則會阻塞HTML的解析。
  3. 如果必須要在頭部增加內聯指令碼,一定要放在CSS標籤之前。

JS載入

對於大型的網站來說,在此我們先提出幾個問題:

  • 將全部JS程式碼打成一個包,造成首次執行程式碼過大怎麼辦?
  • 將JS以細粒度打包,造成請求過多怎麼辦?
  • 將JS按 "基礎庫" + "頁面程式碼" 分別打包,要怎麼界定什麼是基礎程式碼,什麼是頁面程式碼;不同頁面用的基礎程式碼不一致怎麼辦?
  • 單一檔案的少量程式碼改的是否會導致快取失效?
  • 程式碼模組間有動態依賴,怎樣合併請求。

關於這些問題的解決方案數量可能會比問題還多,而它們也各有優劣。

具體分析太過複雜,鑑於篇幅原因在這裡不做具體分析了。您可以期待我們的後續計劃:BPM(瀏覽器包管理)。

JS解析、編譯、執行

在PC網際網路時代,人們似乎都快忘記了JS的解析和執行還需要消耗時間。確實,在幾年前網速還在用kb衡量的時代裡,JS的解析時間在整個頁面的開啟時間裡只能算是九牛一毛。

然而,隨著網速越來越快,而CPU的速度反而沒有提升(從PC到手機),JS的時間開銷就成為問題了。那麼JS的編譯和解析,在當今的頁面上要消耗多少時間呢?

分析

我們用以下方式來檢驗JS程式碼的解析/編譯和執行時間:

<script>
    window.t1 = performance.now()
</script>
<script>
    window.test = function () {
        // test code
    }
</script>
<script>
    window.t2 = performance.now()
    test();
    window.t3 = performance.now();

    alert("編譯耗時:" + (t2 - t1));
    alert("執行耗時:" + (t3 - t2));
</script>

將測試程式碼放入 【test code】 位置,然後在手機中執行;

  • 在t1~t2期間,JS程式碼僅僅聲明瞭一個函式,主要時間會集中在解析和編譯過程;
  • 在t2~t3時間段內,執行test時時間主要為程式碼的執行時間

在首次啟動客戶端後,開啟WebView的測試頁面,我們可以得到如下的結果:

測試系統: iPhone6 iOS 10.2.1 測試系統: OPPO R829T Android 4.2.2 內容值: 編譯時間(ms)/執行時間(ms)

系統

Zepto.js

Vue.js

React.js + ReactDOM.js

iOS

5.2 / 8

12.8 / 16.1

13.7 / 43.3

Android

13 / 40

43 / 127

26 / 353

當保持客戶端進行不關閉情況下,關閉WebView並重新訪問測試頁面,再次測試得到如下結果:

系統

Zepto.js

Vue.js

React.js + ReactDom.js

iOS

0.9 / 1.9

5 / 7.4

3.5 / 23

Android

5 / 9

17 / 12

25 / 60

執行時間指的是框架程式碼載入的頁面的初始化時間,沒有任何業務的呼叫。

這意味著什麼

經過測試可以得出以下結論:

  • 偏重的框架,例如React,僅僅初始化的時間就會達到50ms ~ 350ms,這在對效能敏感的業務中時比較不利的。
  • 在App的啟動週期內,統一域名下的程式碼會被快取編輯和初始化結果,重複呼叫效能較好。

所以,在移動瀏覽器上,JS的解析和執行時間並不是不可忽略的。

在低端安卓機上,(框架的初始化+非同步資料請求+業務程式碼執行)會遠高於幾KB網路請求時間;高效能的Web網站需要仔細斟酌前端渲染帶來的效能問題。

優化

  • 高效能要求頁面還是需要後端渲染。
  • React還是太重了,面向使用者寫系統需要謹慎考慮。
  • JS程式碼的編譯和執行會有快取,同App中網頁儘量統一框架。

WebView效能優化總結

一個載入網頁的過程中,native、網路、後端處理、CPU都會參與,各自都有必要的工作和依賴關係;讓他們相互並行處理而不是相互阻塞才可以讓網頁載入更快:

  • WebView初始化慢,可以在初始化同時先請求資料,讓後端和網路不要閒著。
  • 後端處理慢,可以讓伺服器分trunk輸出,在後端計算的同時前端也載入網路靜態資源。
  • 指令碼執行慢,就讓指令碼在最後執行,不阻塞頁面解析。
  • 同時,合理的預載入、預快取可以讓載入速度的瓶頸更小。
  • WebView初始化慢,就隨時初始化好一個WebView待用。
  • DNS和連結慢,想辦法複用客戶端使用的域名和連結。
  • 指令碼執行慢,可以把框架程式碼拆分出來,在請求頁面之前就執行好。

WebView記憶體消耗

分析

為了測試WebView會消耗多少記憶體,我們設計瞭如下的測試方案:

  1. 客戶端啟動後,記錄消耗的記憶體。
  2. 開啟空頁面,記錄記憶體的上漲。
  3. 退出。
  4. 開啟空頁面,記錄記憶體上漲。
  5. 退出。
  6. 開啟載入了程式碼的頁面,記錄記憶體的額外增加。

得到如下測試結果:

測試系統: iOS模擬器,Titans 10.0.7 測試系統: OPPO R829T Android 4.2.2 測試方式:測試10次取平均值

首次開啟增加記憶體

二次開啟增加記憶體

載入KNB+VUE+靈犀

iOS UIWebView

31.1M

5.52M

2M

iOS WKWebView

1.95M

1.6M

2M

Android

32.2M

6.62M

1.7M

WKWebView的記憶體消耗相比其他低了一個數量級,在此方面相當佔優。

UIWebView和Android的WebView在首次初始化時都要消耗大量記憶體,之後每次新建WebView會額外增加一些。

UIWebView的記憶體佔用不會在關閉WebView時主動回收,每次新開WebView都會消耗額外記憶體。

相比於效能,對於記憶體的優化可以做的還是比較有限的。

  • WKWebView的記憶體佔用優勢比較大(代價是初始化比較慢)。
  • 頁面內程式碼消耗的記憶體相比與WebView系統的記憶體消耗相比可以說是很低。

WebView體驗

除了開啟的速度,WebView通常體驗也沒有native的實現更好,我們可以找到以下幾個例子:

長按選擇

在WebView中,長按文字會使得WebView預設開始選擇文字;長按連結會彈出提示是否在新頁面開啟。

解決方法:可以通過給body增加CSS來禁止這些預設規則。

點選延遲

在WebView中,click通常會有大約300ms的延遲(同時包括連結的點選,表單的提交,控制元件的互動等任何使用者點選行為)。

唯一的例外是設定的meta:viewpoint為禁止縮放的Chrome(然而並不是Android預設的瀏覽器)。

解決方法:使用fastclick一般可以解決這個問題。

頁面滑動期間不渲染/執行

在很多需求中會有一些吸頂的元素,例如導航條,購買按鈕等;當頁面滾動超出元素高度後,元素吸附在螢幕頂部。

這個功能在PC和native中都能夠實現,然而在WebView中卻成了難題:

 在頁面滾動期間,Scroll Event不觸發

不僅如此,WebView在滾動期間還有各種限定:

  • setTimeout和setInterval不觸發。
  • GIF動畫不播放。
  • 很多回調會延遲到頁面停止滾動之後。
  • background-position: fixed不支援。
  • 這些限制讓WebView在滾動期間很難有較好的體驗。

這些限制大部分是不可突破的,但至少對於吸頂功能還是可以做一些支援:

解決方法:

  • 在iOS上,使用position: sticky可以做到元素吸頂。
  • 在Android上,監聽touchmove事件可以在滑動期間做元素的position切換(慣性運動期間就無效了)。

鍵盤形態有限

WebView對鍵盤的控制能力很弱,無法直接調起或者隱藏鍵盤,而且鍵盤的確認文案是無法自定義的。

我們以百度為例:

當你開啟百度搜索時,點選【換行】就完成了輸入並開始了搜尋。

為什麼是【換行】而不是【搜尋】呢?

當然不是bug……而是……臣妾做不到啊!

解決方法:

目前只能通過由與App通過橋協議的方式,由App代為喚起鍵盤(但是實際操作過於複雜)。

crash

通常WebView並不能直接接觸到底層的API,因此比較穩定;但仍然有使用不當造成整個App崩潰的情況。

目前發現的案例包括:

  • 使用過大的圖片(2M)
  • 不正常使用WebGL

WebView安全

WebView被運營商劫持、注入問題

由於WebView載入的頁面程式碼是從伺服器動態獲取的,這些程式碼將會很容易被中間環節所竊取或者修改,其中最主要的問題出自地方運營商(浙江尤其明顯)和一些WiFi。

我們監測到的問題包括:

  • 無視通訊規則強制快取頁面。
  • header被篡改。
  • 頁面被注入廣告。
  • 頁面被重定向。
  • 頁面被重定向並重新iframe到新頁面,框架嵌入廣告。
  • HTTPS請求被攔截。
  • DNS劫持。

這些問題輕則影響使用者體驗,重則洩露資料,或影響公司信譽。

針對頁面注入的行為,有一些解決方案:

使用CSP(Content Security Policy)

CSP可以有效的攔截頁面中的非白名單資源,而且相容性較好。在美團移動版的使用中,能夠阻止大部分的頁面內容注入。

但在使用中還是存在以下問題:

  • 由於業務的需要,通常inline指令碼還是在白名單中,會導致完全依賴內聯的頁面程式碼注入可以通過檢測。
  • 如果注入的內容是純HTML+CSS的內容,則CSP無能為力。
  • 無法解決頁面被劫持的問題。
  • 會帶來額外的一些維護成本。

總體來說CSP是一個行之有效的防注入方案,但是如果對於安全要求更高的網站,這些還不夠。

HTTPS

HTTPS可以防止頁面被劫持或者注入,然而其副作用也是明顯的,網路傳輸的效能和成功率都會下降,而且HTTPS的頁面會要求頁面內所有引用的資源也是HTTPS的,對於大型網站其遷移成本並不算低。

HTTPS的一個問題在於:一旦底層想要篡改或者劫持,會導致整個連結失效,頁面無法展示。這會帶來一個問題:本來頁面只是會被注入廣告,而且廣告會被CSP攔截,而採用了HTTPS後,整個網頁由於受到劫持完全無法展示。

對於安全要求不高的靜態頁面,就需要權衡HTTPS帶來的利與弊了。

App使用Socket代理請求

如果HTTP請求容易被攔截,那麼讓App將其轉換為一個Socket請求,並代理WebView的訪問也是一個辦法。

通常不法運營商或者WiFi都只能攔截HTTP(S)請求,對於自定義的包內容則無法攔截,因此可以基本解決注入和劫持的問題。

Socket代理請求也存在問題。

  • 首先,使用客戶端代理的頁面HTML請求將喪失邊下載邊解析的能力;根據前面所述,瀏覽器在HTML收到部分內容後就立刻開始解析,並載入解析出來的外鏈、圖片等,執行內聯的指令碼……而目前WebView對外並沒有暴露這種流式的HTML介面,只能由客戶端完全下載好HTML後,注入到WebView中。因此其效能將會受到影響。
  • 其次,其技術問題也是較多的,例如對跳轉的處理,對快取的處理,對CDN的處理等等……稍不留神就會埋下若干大坑。

此外還有一些其他的辦法,例如頁面的MD5檢測,頁面靜態頁打包下載等等方式,具體如何選擇還要根據具體的場景抉擇。

客戶端內開啟第三方WebView

一般來說,客戶端內的WebView都是可以通過客戶端的某個schema開啟的,而要開啟頁面的URL很多都並不寫在客戶端內,而是可以由URL中的引數傳遞過去的。

那麼,一旦此URL可以通過外界輸入自定義,那麼就有可能在客戶端內部開啟一個外部的網頁。

例:作案過程

  • 某個App有個WebView,開啟的schema為 appxx://web?url={weburl}。
  • App中有個掃碼的功能,可以掃描某個二維碼並開啟對應的schema連結。
  • 某個壞人制作了一個二維碼並張貼到街上,內容符合 : appxx://web?url={some_hack_weburl}。
  • 使用者掃碼打開了some_hack_weburl。
  • 如果some_hack_weburl是一個高仿的登入頁面,那麼使用者將會很可能將使用者名稱密碼提交到其他網站。

解決方法:在內嵌的WebView中應該限制允許開啟的WebView的域名,並設定執行訪問的白名單。或者當用戶開啟外部連結前給使用者強烈而明顯的提示。

發展

在一個客戶端內,native目前主要功能是提供高效而基礎的功能;內部的WebView則新增一些效能體驗要求不高但動態化要求高的能力。

提高客戶端的動態能力,或者提高WebView的效能,都是提升App功能覆蓋的方式。

而目前的各種框架,ReactNative、Week包括微信小程式,都是這個趨勢的嘗試。

隨著技術的發展,WebView的效能、體驗和安全問題也將會逐漸的改善,在App中佔有越來越多比重的同時,也將會為App開拓新的能力,為使用者帶來更優質的體驗。