在App開發中,內嵌WebView始終佔有着一席之地。它能以較低的成本實現Android、iOS和Web的複用,也能夠堂而皇之的突破蘋果對熱更新的封鎖。css
然而便利性的同時,WebView的性能體驗卻備受質疑,致使不少客戶端中須要動態更新等頁面時不得不採用其餘方案。前端
以發展的眼光來看,功能的動態加載以及三端的融合將會是大趨勢。vue
那麼如何克服WebView固有的問題呢?react
咱們將從性能、內存消耗、體驗、安全幾個維度,來系統的分析客戶端默認WebView的問題,以及對應的優化方案。web
性能
對於WebView的性能,給人最直觀的莫過於:打開速度比native慢。小程序
是的,當咱們打開一個WebView頁面,頁面每每會慢吞吞的loading好久,若干秒後纔出現你所須要看到的頁面。後端
這是爲何呢?微信小程序
對於一個普通用戶來說,打開一個WebView一般會經歷如下幾個階段:api
- 交互無反饋
- 到達新的頁面,頁面白屏
- 頁面基本框架出現,可是沒有數據;頁面處於loading狀態
- 出現所需的數據
若是從程序上觀察,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 的架構如何優化演進?】
還有其餘各類優化的方式,再也不一一列舉,總結起來都是圍繞兩點:
- 在使用前 預先初始化 好WebView,從而減少耗時。
- 在初始化的同時,經過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通常有兩個做用:
- 肯定靜態資源的版本。
- 根據用戶的請求,去業務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放錯位置 也會讓性能降低不少。
- CSS的加載 會在HTML解析 到CSS的標籤時開始,因此CSS的標籤要儘可能靠前。
- 可是,CSS連接下面不能有 任何的JS標籤( 包括很簡單的內聯JS),不然會阻塞HTML的解析。
- 若是必需要在頭部增長內聯腳本, 必定要放在CSS標籤以前。 ???
JS加載
對於大型的網站來講,在此咱們先提出幾個問題:
- 將所有JS代碼打成一個包,形成首次執行代碼過大怎麼辦?
- 將JS以細粒度打包,形成請求過多怎麼辦?
- 將JS按 "基礎庫" + "頁面代碼" 分別打包,要怎麼界定什麼是基礎代碼,什麼是頁面代碼;不一樣頁面用的基礎代碼不一致怎麼辦?
- 單一文件的少許代碼改的是否會致使緩存失效?
- 代碼模塊間有動態依賴,怎樣合併請求。
關於這些問題的解決方案數量可能會比問題還多,而它們也各有優劣。
具體分析太過複雜,鑑於篇幅緣由在這裏不作具體分析了。
您能夠期待咱們的後續計劃:BPM(瀏覽器包管理)。
JS解析、編譯、執行
在PC互聯網時代,人們彷佛都快忘記了 JS的解析和執行 還須要消耗時間。
確實,在幾年前網速還在用kb衡量的時代裏,JS的解析時間在整個頁面的打開時間裏只能算是九牛一毛。
然而,隨着網速愈來愈快, 而 CPU的速度反而沒有提高(從PC到手機),JS的時間開銷就成爲問題了。
解析執行js的速度 依賴好的cpu
那麼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的測試頁面,咱們能夠獲得以下的結果: vue在安卓下仍是要100多毫秒去執行, 雖然只是react 2/5
測試系統: 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會消耗多少內存,咱們設計了以下的測試方案:
- 客戶端啓動後,記錄消耗的內存。
- 打開空頁面,記錄內存的上漲。
- 退出。
- 打開空頁面,記錄內存上漲。
- 退出。
- 打開加載了代碼的頁面,記錄內存的額外增長。
獲得以下測試結果:
測試系統: 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切換(慣性運動期間就無效了)。
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開拓新的能力,爲用戶帶來更優質的體驗。
發現文章有錯誤、對內容有疑問,均可以關注美團技術團隊微信公衆號(meituantech),在後臺給咱們留言。咱們每週會挑選出一位熱心小夥伴,送上一份精美的小禮品。快來掃碼關注咱們吧!