業(yè)界衡量移動web app交互性能的優(yōu)劣主要是通過監(jiān)測webview渲染頁面時白屏?xí)r間,DOM樹構(gòu)建時間,整頁時間和首屏?xí)r間這三個指標(biāo)來完成的,那么這四個指標(biāo)分別的意義是什么呢?我們從w3c提供的navigation Timing中看到交互性能指的是Processing和onLoad這兩部分的時間。

在瀏覽器交互階段(Processing和onLoad時間段)瀏覽器接收服務(wù)器返回的基礎(chǔ)頁數(shù)據(jù)后,瀏覽器需要對HTML這個單純的文本內(nèi)容進(jìn)行解析,從文本中構(gòu)建出一個內(nèi)部數(shù)據(jù)結(jié)構(gòu),叫做DOM樹(DOM tree),用于組織將要繪制在屏幕上的內(nèi)容。從HTML也能得到外聯(lián)或內(nèi)聯(lián)的CSS腳本和JavaScript腳本,當(dāng)然還有媒體文件,比如圖片、視頻、聲音,這些都需要再次發(fā)起網(wǎng)絡(luò)請求下載。CSS文本內(nèi)容中的規(guī)則同樣會被構(gòu)建成一個內(nèi)部數(shù)據(jù)結(jié)構(gòu),叫做CSS樹(CSS tree),來決定DOM樹的節(jié)點(diǎn)在屏幕上的布局、顏色、狀態(tài)效果。JavaScript腳本被觸發(fā)執(zhí)行后,除了計算業(yè)務(wù),往往還需要操作DOM樹,就是所謂的DOM API。

白屏?xí)r間
指瀏覽器開始顯示內(nèi)容的時間。但是在傳統(tǒng)的采集方式里,是在HTML的head標(biāo)簽結(jié)尾里記錄時間戳,來計算白屏?xí)r間。在這個時刻,瀏覽器開始解析body標(biāo)簽內(nèi)的內(nèi)容。而現(xiàn)代瀏覽器不會等待CSS樹(所有CSS文件下載和解析完成)和DOM樹(整個body標(biāo)簽解析完成)構(gòu)建完成才開始繪制,而是馬上開始顯示中間結(jié)果。所以經(jīng)常在低網(wǎng)速的環(huán)境中,觀察到頁面由上至下緩慢顯示完,或者先顯示文本內(nèi)容后再重繪成帶有格式的頁面內(nèi)容。在android中我們通過使用webview.WebChromeClient的onReceivedTitle事件來近似獲得白屏?xí)r間。DOM樹構(gòu)建時間
指瀏覽器開始對基礎(chǔ)頁文本內(nèi)容進(jìn)行解析到從文本中構(gòu)建出一個內(nèi)部數(shù)據(jù)結(jié)構(gòu)(DOM樹)的時間,這個事件是從HTML中的onLoad的延伸而來的,當(dāng)一個頁面完成加載時,初始化腳本的方法是使用load事件,但這個類函數(shù)的缺點(diǎn)是僅在所有資源都完全加載后才被觸發(fā),這有時會導(dǎo)致比較嚴(yán)重的延遲,開發(fā)人員隨后創(chuàng)建了domready事件,它在DOM加載之后及資源加載之前被觸發(fā)。domready被眾多JavaScript庫所采用,它在本地瀏覽器中以DOMContentLoaded事件的形式被使用。在android中我們通過注入js代碼到webview中的方式來實(shí)現(xiàn);具體實(shí)現(xiàn)上,在WebChromeClient的onReceivedTitle事件被觸發(fā)時注入我們的js代碼,然后通過WebChromeClient的onJsPrompt事件來獲取domc(window.DOMContentLoaded事件)時間。
@Override
public void onReceivedTitle (WebView view, String title) {
view.loadUrl("javascript:" +
"window.addEventListener('DOMContentLoaded', function() {" +
"prompt('domc:' + new Date().getTime());" +
);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) {
Log.i(UAQ_WEB_ACTIVITY, "**** Blocking Javascript Prompt :" + message);
if(message != null){
if(!preCacheRun){
String[] strs = message.split(":");
if(2 == strs.length){
if("domc".equals(strs[0])){
result.getCurrentRun().setDocComplete(Long.valueOf(strs[1].trim()));
}
}
}
}
r.confirm(defaultValue);
return true;
}
- 首屏?xí)r間
指從網(wǎng)頁應(yīng)用的角度定義的指標(biāo),在Navigation Timing或者瀏覽器實(shí)現(xiàn)中并沒有相關(guān)指標(biāo)值。首屏?xí)r間,是指用戶看到第一屏,即整個網(wǎng)頁頂部大小為當(dāng)前窗口的區(qū)域,顯示完整的時間。常用的方法有,頁面標(biāo)簽標(biāo)記法、圖像相似度比較法和首屏高度內(nèi)圖片加載法;
- 頁面標(biāo)簽標(biāo)記法,在HTML文檔中對應(yīng)首屏內(nèi)容的標(biāo)簽結(jié)束位置,使用內(nèi)聯(lián)的JavaScript代碼記錄當(dāng)前時間戳,比較局限;
- 圖像相似度比較法,通過比較連續(xù)截屏圖像的像素點(diǎn)變化趨勢確定首屏?xí)r間,最為科學(xué)和直觀的方式,但是比較消耗本地設(shè)備的運(yùn)行資源;
- 首屏高度內(nèi)圖片加載法,通過尋找首屏區(qū)域內(nèi)的所有圖片,計算它們加載完的時間去得到首屏?xí)r間,這樣比較符合網(wǎng)頁的實(shí)際體驗(yàn)并且比較節(jié)省設(shè)備運(yùn)行資源;
具體實(shí)現(xiàn)上我采用的是最后一種,即“首屏高度內(nèi)圖片加載法”;因?yàn)橥ǔP枰紤]首屏?xí)r間的頁面,都是因?yàn)樵谑灼廖恢脙?nèi)放入了較多的圖片資源?,F(xiàn)代瀏覽器處理圖片資源時是異步的,會先將圖片長寬應(yīng)用于頁面排版,然后隨著收到圖片數(shù)據(jù)由上至下繪制顯示的。并且瀏覽器對每個頁面的TCP連接數(shù)限制,使得并不是所有圖片都能立刻開始下載和顯示。因此我們在DOM樹構(gòu)建完成后即可遍歷獲得所有在設(shè)備屏幕高度內(nèi)的所有圖片資源標(biāo)簽,在所有圖片標(biāo)簽中添加document.onload事件,在整頁加載完成(window.onLoad事件發(fā)生)時遍歷圖片標(biāo)簽并獲得之前注冊的document.onload事件時間的最大值,該最大值減去navigationStart即認(rèn)為近似的首屏?xí)r間。在android中我們通過注入js代碼到webview中的方式來實(shí)現(xiàn);具體實(shí)現(xiàn)上,在WebChromeClient的onReceivedTitle事件被觸發(fā)時注入我們的js代碼,然后通過WebChromeClient的onJsPrompt事件來獲取firstscreen時間。
js部分計算首屏?xí)r間的邏輯代碼:
function first_screen () {
var imgs = document.getElementsByTagName("img"), fs = +new Date;
var fsItems = [], that = this;
function getOffsetTop(elem) {
var top = 0;
top = window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop;
try{
top += elem.getBoundingClientRect().top;
}catch(e){
}finally{
return top;
}
}
var loadEvent = function() {
//gif避免
if (this.removeEventListener) {
this.removeEventListener("load", loadEvent, false);
}
fsItems.push({
img : this,
time : +new Date
});
}
for (var i = 0; i < imgs.length; i++) {
(function() {
var img = imgs[i];
if (img.addEventListener) {
!img.complete && img.addEventListener("load", loadEvent, false);
} else if (img.attachEvent) {
img.attachEvent("onreadystatechange", function() {
if (img.readyState == "complete") {
loadEvent.call(img, loadEvent);
}
});
}
})();
}
function firstscreen_time() {
var sh = document.documentElement.clientHeight;
for (var i = 0; i < fsItems.length; i++) {
var item = fsItems[i], img = item['img'], time = item['time'], top = getOffsetTop(img);
if (top > 0 && top < sh) {
fs = time > fs ? time : fs;
}
}
return fs;
}
window.addEventListener('load', function() {
prompt('firstscreen:' + firstscreen_time());
});
}
webview的注入代碼:
private void registerOnLoadHandler(WebView view) {
String jscontent = getJavaScriptAsString();
view.loadUrl("javascript:" + jscontent +
"window.addEventListener('DOMContentLoaded', function() {" +
"first_screen();
});"
);
}
@Override
public void onReceivedTitle (WebView view, String title) {
registerOnLoadHandler(view);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) {
Log.i(UAQ_WEB_ACTIVITY, "**** Blocking Javascript Prompt :" + message);
if(message != null){
if(!preCacheRun){
String[] strs = message.split(":");
if(2 == strs.length){
if("firstscreen".equals(strs[0])){
result.getCurrentRun().setFirstScreen(Long.valueOf(strs[1].trim()));
}
}
}
}
r.confirm(defaultValue);
return true;
}
- 整頁時間
指頁面完成整個加載過程的時刻。從Navigation Timing API上采集,就是loadEventEnd減去navigationStart。在傳統(tǒng)采集方法中,會使用window對象的onload事件來記錄時間戳,它表示瀏覽器認(rèn)定該頁面已經(jīng)載入完全了。android中我們通過注入js代碼到webview中的方式來實(shí)現(xiàn);具體實(shí)現(xiàn)上,在WebChromeClient的onReceivedTitle事件被觸發(fā)時注入我們的js代碼,然后通過WebChromeClient的onJsPrompt事件來獲取load(window.onLoad事件)時間。
@Override
public void onReceivedTitle (WebView view, String title) {
view.loadUrl("javascript:" +
"window.addEventListener('load', function() {" +
"prompt('load:' + new Date().getTime());" +
);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) {
Log.i(UAQ_WEB_ACTIVITY, "**** Blocking Javascript Prompt :" + message);
if(message != null){
if(!preCacheRun){
String[] strs = message.split(":");
if(2 == strs.length){
if("load".equals(strs[0])){
result.getCurrentRun().setFullyLoaded(Long.valueOf(strs[1].trim()));
}
}
}
}
r.confirm(defaultValue);
return true;
}
歡迎關(guān)注我的個人訂閱號
