在App開發(fā)中直撤,內(nèi)嵌WebView始終占有著一席之地。它能以較低的成本實現(xiàn)Android型豁、iOS和Web的復用,也可以冠冕堂皇的突破蘋果對熱更新的封鎖尚蝌。
然而便利性的同時迎变,WebView的性能體驗卻備受質疑,導致很多客戶端中需要動態(tài)更新等頁面時不得不采用其他方案飘言。
以發(fā)展的眼光來看衣形,功能的動態(tài)加載以及三端的融合將會是大趨勢。那么如何克服WebView固有的問題呢姿鸿?我們將從性能谆吴、內(nèi)存消耗、體驗般妙、安全幾個維度纪铺,來系統(tǒng)的分析客戶端默認WebView的問題,以及對應的優(yōu)化方案碟渺。
對于WebView的性能鲜锚,給人最直觀的莫過于:打開速度比native慢突诬。
是的,當我們打開一個WebView頁面芜繁,頁面往往會慢吞吞的loading很久旺隙,若干秒后才出現(xiàn)你所需要看到的頁面。
這是為什么呢骏令?
對于一個普通用戶來講蔬捷,打開一個WebView通常會經(jīng)歷以下幾個階段:
- 交互無反饋
- 到達新的頁面,頁面白屏
- 頁面基本框架出現(xiàn)榔袋,但是沒有數(shù)據(jù)周拐;頁面處于loading狀態(tài)
-
出現(xiàn)所需的數(shù)據(jù)
如果從程序上觀察,WebView啟動過程大概分為以下幾個階段:
如何縮短這些過程的時間凰兑,就成了優(yōu)化WebView性能的關鍵妥粟。
接下來我們逐一分析各個階段的耗時情況,以及需要注意的優(yōu)化點吏够。
WebView初始化
當App首次打開時勾给,默認是并不初始化瀏覽器內(nèi)核的;只有當創(chuàng)建WebView實例的時候锅知,才會創(chuàng)建WebView的基礎框架播急。
所以與瀏覽器不同,App中打開WebView的第一步并不是建立連接售睹,而是啟動瀏覽器內(nèi)核桩警。
我們來分析一下這段耗時到底需要多久。
分析
針對WebView的初始化時間侣姆,我們可以定義兩個指標:
測試內(nèi)容:
首次初始化時間:客戶端冷啟動后生真,第一次打開WebView沉噩,從開始創(chuàng)建WebView到開始建立網(wǎng)絡連接之間的時間捺宗。
二次初始化時間:在打開過WebView后,退出WebView川蒙,再重新打開WebView蚜厉,從開始創(chuàng)建WebView到開始建立網(wǎng)絡連接之間的時間。
測試數(shù)據(jù):
測試系統(tǒng)1: iOS模擬器畜眨,Titans 10.0.7
測試系統(tǒng)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時間
怎么優(yōu)化
由于這段過程發(fā)生在native的代碼中,單純靠前端代碼是無法優(yōu)化的康聂;大部分的方案都是前端和客戶端協(xié)作完成贰健,以下是幾個業(yè)界采用過的方案。
1. 全局WebView
方法:
在客戶端剛啟動時恬汁,就初始化一個全局的WebView待用伶椿,并隱藏;
當用戶訪問了WebView時,直接使用這個WebView加載對應網(wǎng)頁脊另,并展示导狡。
這種方法可以比較有效的減少WebView在App中的首次打開時間。當用戶訪問頁面時偎痛,不需要初始化WebView的時間旱捧。
參考:https://patents.google.com/patent/CN106250434A/zh
當然這也帶來了一些問題,包括:
- 額外的內(nèi)存消耗踩麦。
- 頁面間跳轉需要清空上一個頁面的痕跡枚赡,更容易內(nèi)存泄露。
2. 客戶端代理數(shù)據(jù)請求
方法:
在客戶端初始化WebView的同時谓谦,直接由native開始網(wǎng)絡請求數(shù)據(jù)标锄;
當頁面初始化完成后,向native獲取其代理請求的數(shù)據(jù)茁计。
此方法雖然不能減小WebView初始化時間料皇,但數(shù)據(jù)請求和WebView初始化可以并行進行,總體的頁面加載時間就縮短了星压;縮短總體的頁面加載時間:
參考:https://mp.weixin.qq.com/s/evzDnTsHrAr2b9jcevwBzA?
還有其他各種優(yōu)化的方式践剂,不再一一列舉,總結起來都是圍繞兩點:
- 在使用前預先初始化好WebView娜膘,從而減小耗時逊脯。
- 在初始化的同時,通過Native來完成一些網(wǎng)絡請求等過程竣贪,使得WebView初始化不是完全的阻塞后續(xù)過程军洼。
建立連接/服務器處理
在頁面請求的數(shù)據(jù)返回之前,主要有以下過程耗費時間演怎。
- DNS
- connection
- 服務器處理
優(yōu)化
這些時間都是發(fā)生在網(wǎng)頁加載之前匕争,但這并不意味著無法優(yōu)化,有以下幾種方法爷耀。
DNS采用和客戶端API相同的域名
DNS會在系統(tǒng)級別進行緩存甘桑,對于WebView的地址,如果使用的域名與native的API相同歹叮,則可以直接使用緩存的DNS而不用再發(fā)起請求圖片跑杭。
以美團為例,美團的客戶端請求域名主要位于api.meituan.com咆耿,然而內(nèi)嵌的WebView主要位于 i.meituan.com德谅。
當我們初次打開App時:
- 客戶端首次打開都會請求api.meituan.com,其DNS將會被系統(tǒng)緩存萨螺。
- 然而當打開WebView的時候窄做,由于請求了不同的域名宅荤,需要重新獲取i.meituan.com的IP。
根據(jù)上面的統(tǒng)計浸策,至少10%的用戶打開WebView時耗費了60ms在DNS上面冯键,如果WebView的域名與App的API域名統(tǒng)一,則可以讓WebView的DNS時間全部達到1.3ms的量級庸汗。
靜態(tài)資源同理惫确,最好與客戶端的資源域名保持一致。
同步渲染采用chunk編碼
同步渲染時如果后端請求時間過長蚯舱,可以考慮采用chunk編碼改化,將數(shù)據(jù)放在最后,并優(yōu)先將靜態(tài)內(nèi)容flush枉昏。對于傳統(tǒng)的后端渲染頁面陈肛,往往都是使用的【瀏覽器】–> 【W(wǎng)eb API】 –> 【業(yè)務 API】的加載模式,其中后端時間就指的是Web API的處理時間了兄裂。在這里Web API一般有兩個作用:
- 確定靜態(tài)資源的版本句旱。
- 根據(jù)用戶的請求,去業(yè)務API獲取數(shù)據(jù)晰奖。
而一般確定靜態(tài)資源的版本往往是直接讀取代碼版本谈撒,基本無耗時;而主要的后端時間都花費在了業(yè)務API請求上面匾南。
那么怎么優(yōu)化利用這段時間呢啃匿?
在HTTP協(xié)議中,我們可以在header中設置 transfer-encoding:chunked 使得頁面可以分塊輸出蛆楞。如果合理設計頁面溯乒,讓head部分都是確定的靜態(tài)資源版本相關內(nèi)容,而body部分是業(yè)務數(shù)據(jù)相關內(nèi)容豹爹,那么我們可以在用戶請求的時候裆悄,首先將Web API可以確定的部分先輸出給瀏覽器,然后等API完全獲取后帅戒,再將API數(shù)據(jù)傳輸給瀏覽器灯帮。
下圖可以直觀的看出分chunk輸出和一起輸出的區(qū)別:
- 如果采用普通方式輸出頁面,則頁面會在服務器請求完所有API并處理完成后開始傳輸逻住。瀏覽器要在后端所有API都加載完成后才能開始解析。
- 如果采用chunk-encoding: chunked迎献,并優(yōu)先將頁面的靜態(tài)部分輸出瞎访;然后處理API請求,并最終返回頁面吁恍,可以讓后端的API請求和前端的資源加載同時進行扒秸。
- 兩者的總共后端時間并沒有區(qū)別播演,但是可以提升首字節(jié)速度,從而讓前端加載資源和后端加載API不互相阻塞伴奥。
頁面框架渲染
頁面在解析到足夠多的節(jié)點写烤,且所有CSS都加載完成后進行首屏渲染。在此之前拾徙,頁面保持白屏洲炊;在頁面完全下載并解析完成之前,頁面處于不完整展示狀態(tài)尼啡。
分析
我們以一個美團的活動頁面作為樣例:
測試頁面:http://i.meituan.com/firework/meituanxianshifengqiang
在Mac上面暂衡,模擬4G情況
頁面樣式:
測試得到的時間耗費如下:
表1
階段 | 時間 | 大小 | 備注 |
---|---|---|---|
DOM下載 | 58ms | 29.5?KB | 4G網(wǎng)絡 |
DOM解析 | 12.5ms | 198?KB | 根據(jù)估算,在手機上慢2~5倍不等 |
CSS請求+下載 | 58ms | 11.7?KB | 4G網(wǎng)絡(包含鏈接時間崖瞭,CDN) |
CSS解析 | 2.89ms | 54.1?KB | 根據(jù)估算狂巢,在手機上慢2~5倍不等 |
渲染 | 23ms | 1361節(jié)點 | 根據(jù)估算,在手機上慢2~5倍不等 |
繪制 | 4.1ms | 根據(jù)估算书聚,在手機上慢2~5倍不等 | |
合成 | 0.23ms | GPU處理 |
同時唧领,對HTML的加載時間進行分析,可以得到如下時間點雌续。
表2
指標 | 時間 | 計算方法 |
---|---|---|
HTML加載完成時間 | 218 | performance.timing.responseEnd - performance.timing.fetchStart |
HTML解析完成時間 | 330 | performance.timing.domInteractive - performance.timing.fetchStart |
這意味著什么呢疹吃?
對于表1
可以看到,隨著在網(wǎng)絡優(yōu)良的情況下西雀,Dom的解析所占耗時比例還是不算低的萨驶,對于低端機器更甚。Layout時間也是首屏前耗時的大頭艇肴,據(jù)猜測這與頁面使用了rem作為單位有關(待進一步分析)腔呜。
對于表2,我們可以發(fā)現(xiàn)一個問題
一般來說HTML在開始接收到返回數(shù)據(jù)的時候就開始解析HTML并構建DOM樹再悼。如果沒有JS(JavaScript)阻塞的話一般會相繼完成核畴。然而,在這里時間相差了90ms……也就是說冲九,解析被阻塞了谤草。
進一步分析可以發(fā)現(xiàn),頁面的header部分有這樣的代碼:
.....
<link 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部分如果單獨出現(xiàn)丑孩,都不會阻塞頁面的解析:
CSS不會阻止頁面繼續(xù)向下繼續(xù)。
內(nèi)聯(lián)的JS很快執(zhí)行完成灭贷,然后繼續(xù)解析文檔温学。
然而,當這兩部分同時出現(xiàn)的時候甚疟,問題就來了仗岖。CSS加載阻塞了下面的一段內(nèi)聯(lián)JS的執(zhí)行逃延,而被阻塞的內(nèi)聯(lián)JS則阻塞了HTML的解析。
通常情況下轧拄,CSS不會阻塞HTML的解析揽祥,但如果CSS后面有JS,則會阻塞JS的執(zhí)行直到CSS加載完成(即便JS是內(nèi)聯(lián)的腳本)檩电,從而間接阻塞HTML的解析拄丰。
優(yōu)化
在頁面框架加載這一部分,能夠優(yōu)化的點參照雅虎14條就夠了是嗜;但注意不要犯錯愈案,一個小小的內(nèi)聯(lián)JS放錯位置也會讓性能下降很多。
- CSS的加載會在HTML解析到CSS的標簽時開始鹅搪,所以CSS的標簽要盡量靠前站绪。
- 但是,CSS鏈接下面不能有任何的JS標簽(包括很簡單的內(nèi)聯(lián)JS)丽柿,否則會阻塞HTML的解析恢准。
- 如果必須要在頭部增加內(nèi)聯(lián)腳本,一定要放在CSS標簽之前甫题。
JS加載
對于大型的網(wǎng)站來說馁筐,在此我們先提出幾個問題:
- 將全部JS代碼打成一個包,造成首次執(zhí)行代碼過大怎么辦坠非?
- 將JS以細粒度打包敏沉,造成請求過多怎么辦?
- 將JS按 “基礎庫” + “頁面代碼” 分別打包炎码,要怎么界定什么是基礎代碼盟迟,什么是頁面代碼;不同頁面用的基礎代碼不一致怎么辦潦闲?
- 單一文件的少量代碼改的是否會導致緩存失效攒菠?
- 代碼模塊間有動態(tài)依賴,怎樣合并請求歉闰。
關于這些問題的解決方案數(shù)量可能會比問題還多辖众,而它們也各有優(yōu)劣。
具體分析太過復雜和敬,鑒于篇幅原因在這里不做具體分析了凹炸。您可以期待我們的后續(xù)計劃:BPM(瀏覽器包管理)。
JS解析概龄、編譯还惠、執(zhí)行
在PC互聯(lián)網(wǎng)時代,人們似乎都快忘記了JS的解析和執(zhí)行還需要消耗時間私杜。確實蚕键,在幾年前網(wǎng)速還在用kb衡量的時代里,JS的解析時間在整個頁面的打開時間里只能算是九牛一毛衰粹。
然而锣光,隨著網(wǎng)速越來越快,而CPU的速度反而沒有提升(從PC到手機)铝耻,JS的時間開銷就成為問題了誊爹。那么JS的編譯和解析,在當今的頁面上要消耗多少時間呢瓢捉?
分析
我們用以下方式來檢驗JS代碼的解析/編譯和執(zhí)行時間:
<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("執(zhí)行耗時:" + (t3 - t2));
</script>
將測試代碼放入 【test code】 位置频丘,然后在手機中執(zhí)行;
- 在t1~t2期間泡态,JS代碼僅僅聲明了一個函數(shù)搂漠,主要時間會集中在解析和編譯過程;
- 在t2~t3時間段內(nèi)某弦,執(zhí)行test時時間主要為代碼的執(zhí)行時間
在首次啟動客戶端后桐汤,打開WebView的測試頁面,我們可以得到如下的結果:
測試系統(tǒng): iPhone6 iOS 10.2.1
測試系統(tǒng): OPPO R829T Android 4.2.2
內(nèi)容值: 編譯時間(ms)/執(zhí)行時間(ms)
系統(tǒng) | 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并重新訪問測試頁面怔毛,再次測試得到如下結果:
系統(tǒng) | 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 |
執(zhí)行時間指的是框架代碼加載的頁面的初始化時間,沒有任何業(yè)務的調用腾降。
這意味著什么
經(jīng)過測試可以得出以下結論: * 偏重的框架拣度,例如React,僅僅初始化的時間就會達到50ms ~ 350ms螃壤,這在對性能敏感的業(yè)務中時比較不利的抗果。 * 在App的啟動周期內(nèi),統(tǒng)一域名下的代碼會被緩存編輯和初始化結果映穗,重復調用性能較好窖张。
所以,在移動瀏覽器上蚁滋,JS的解析和執(zhí)行時間并不是不可忽略的宿接。
在低端安卓機上,(框架的初始化+異步數(shù)據(jù)請求+業(yè)務代碼執(zhí)行)會遠高于幾KB網(wǎng)絡請求時間辕录;高性能的Web網(wǎng)站需要仔細斟酌前端渲染帶來的性能問題睦霎。
優(yōu)化
- 高性能要求頁面還是需要后端渲染。
- React還是太重了走诞,面向用戶寫系統(tǒng)需要謹慎考慮副女。
- JS代碼的編譯和執(zhí)行會有緩存,同App中網(wǎng)頁盡量統(tǒng)一框架蚣旱。
WebView性能優(yōu)化總結
一個加載網(wǎng)頁的過程中碑幅,native戴陡、網(wǎng)絡、后端處理沟涨、CPU都會參與恤批,各自都有必要的工作和依賴關系;讓他們相互并行處理而不是相互阻塞才可以讓網(wǎng)頁加載更快:
- WebView初始化慢裹赴,可以在初始化同時先請求數(shù)據(jù)喜庞,讓后端和網(wǎng)絡不要閑著。
- 后端處理慢棋返,可以讓服務器分trunk輸出延都,在后端計算的同時前端也加載網(wǎng)絡靜態(tài)資源。
- 腳本執(zhí)行慢睛竣,就讓腳本在最后運行晰房,不阻塞頁面解析。
- 同時酵颁,合理的預加載嫉你、預緩存可以讓加載速度的瓶頸更小。
- WebView初始化慢,就隨時初始化好一個WebView待用。
- DNS和鏈接慢伸辟,想辦法復用客戶端使用的域名和鏈接。
- 腳本執(zhí)行慢距误,可以把框架代碼拆分出來,在請求頁面之前就執(zhí)行好扁位。
WebView被運營商劫持准潭、注入問題
由于WebView加載的頁面代碼是從服務器動態(tài)獲取的,這些代碼將會很容易被中間環(huán)節(jié)所竊取或者修改域仇,其中最主要的問題出自地方運營商(浙江尤其明顯)和一些WiFi刑然。
我們監(jiān)測到的問題包括:
- 無視通信規(guī)則強制緩存頁面。
- header被篡改暇务。
- 頁面被注入廣告泼掠。
- 頁面被重定向。
- 頁面被重定向并重新iframe到新頁面垦细,框架嵌入廣告择镇。
- HTTPS請求被攔截。
- DNS劫持括改。
這些問題輕則影響用戶體驗腻豌,重則泄露數(shù)據(jù),或影響公司信譽。
針對頁面注入的行為吝梅,有一些解決方案:
使用CSP(Content Security Policy)
CSP可以有效的攔截頁面中的非白名單資源虱疏,而且兼容性較好。在美團移動版的使用中憔涉,能夠阻止大部分的頁面內(nèi)容注入订框。
但在使用中還是存在以下問題:
由于業(yè)務的需要析苫,通常inline腳本還是在白名單中兜叨,會導致完全依賴內(nèi)聯(lián)的頁面代碼注入可以通過檢測。
如果注入的內(nèi)容是純HTML+CSS的內(nèi)容衩侥,則CSP無能為力国旷。
無法解決頁面被劫持的問題。
會帶來額外的一些維護成本茫死。
總體來說CSP是一個行之有效的防注入方案跪但,但是如果對于安全要求更高的網(wǎng)站,這些還不夠峦萎。
HTTPS
HTTPS可以防止頁面被劫持或者注入屡久,然而其副作用也是明顯的,網(wǎng)絡傳輸?shù)男阅芎统晒β识紩陆蛋疲襀TTPS的頁面會要求頁面內(nèi)所有引用的資源也是HTTPS的被环,對于大型網(wǎng)站其遷移成本并不算低。
HTTPS的一個問題在于:一旦底層想要篡改或者劫持详幽,會導致整個鏈接失效筛欢,頁面無法展示。這會帶來一個問題:本來頁面只是會被注入廣告唇聘,而且廣告會被CSP攔截版姑,而采用了HTTPS后,整個網(wǎng)頁由于受到劫持完全無法展示迟郎。
對于安全要求不高的靜態(tài)頁面剥险,就需要權衡HTTPS帶來的利與弊了。
App使用Socket代理請求
如果HTTP請求容易被攔截宪肖,那么讓App將其轉換為一個Socket請求表制,并代理WebView的訪問也是一個辦法。
通常不法運營商或者WiFi都只能攔截HTTP(S)請求匈庭,對于自定義的包內(nèi)容則無法攔截夫凸,因此可以基本解決注入和劫持的問題。
Socket代理請求也存在問題阱持。
- 首先夭拌,使用客戶端代理的頁面HTML請求將喪失邊下載邊解析的能力;根據(jù)前面所述,瀏覽器在HTML收到部分內(nèi)容后就立刻開始解析鸽扁,并加載解析出來的外鏈蒜绽、圖片等,執(zhí)行內(nèi)聯(lián)的腳本……而目前WebView對外并沒有暴露這種流式的HTML接口桶现,只能由客戶端完全下載好HTML后躲雅,注入到WebView中。因此其性能將會受到影響骡和。
- 其次相赁,其技術問題也是較多的,例如對跳轉的處理慰于,對緩存的處理钮科,對CDN的處理等等……稍不留神就會埋下若干大坑。
此外還有一些其他的辦法婆赠,例如頁面的MD5檢測绵脯,頁面靜態(tài)頁打包下載等等方式,具體如何選擇還要根據(jù)具體的場景抉擇休里。