當考慮 Web 性能指標時,須要關注的目標數字應該是從您本身的用戶那裏得到的實際用戶指標。最多見的方法是利用 Splunk 之類的工具來分析您的機器數據,該工具支持您分析和可視化您的訪問權限和錯誤日誌。利用這些工具,您能夠收集某些方面的性能數據,好比讀取資產的文件 I/O 時間,以及 API 請求的訪問時間。可是,您仍然須要推斷客戶端性能數據,將信號調用方在某些高級的檢查點上,或者只利用相似 WebPagetest 的工具運行綜合測試。如今,W3C 已將 API 標準化,用戶能夠經過使用 Performance
對象(該對象對於全部現代瀏覽器中的 Windows 對象而言是一個本機對象)捕獲並報告瀏覽器內的性能數據。
javascript
在 2010 年年底,萬維網聯盟 (W3C) 創建了一個新的工做組,即 Web 性能工做組,該工做組提供了用來測量用戶代理特性和 API 的應用程序性能的各個方面的方法。該小組還開發了一個支持將瀏覽器暴露給 JavaScript 的 API,這是一個關鍵的 Web 性能指標。php
在這個 API 中,該工做組建立了大量的新對象和事件,可量化性能指標和優化性能。總的說來,這些對象和界面包括:html
Performance
對象:暴露多個對象,好比 PerformanceNavigation
、PerformanceTiming
和MemoryInfo
,並能記錄高精度時間(high resolution time),從而得到亞毫秒級計時。Page Visibility
API:使您可以肯定某個給定頁面是可見的仍是隱藏的,從而可以優化動畫的內存使用,或優化用於輪詢操做的網絡資源。使用這些對象和界面捕獲瀏覽器內的性能指標並將它們可視化。java
若是在 JavaScript 控制檯中鍵入 window.performance
,則會返回一個類型爲 Performance
的對象,以及該對象所暴露的一些對象和方法。目前,標準的對象集包含:git
window.performance.timing
用於類型 PerformanceTiming
window.performance.navigation
用於類型 PerformanceNavigation
window.performance.memory
用於類型 MemoryInfo
(僅適用於 Chrome 瀏覽器)圖 1 顯示了 Performance
對象的屏幕截圖,可展開該對象來顯示 PerformanceTiming
對象及其屬性。github
Performance
對象Performance
對象被顯示在控制檯中,隨它一塊兒顯示的還有展開的 PerformanceTiming
對象。web
PerformanceTiming
對象PerformanceTiming
對象是以公共屬性的形式暴露的,在瀏覽器中執行檢索和呈現內容的步驟中,它是一個關鍵指標。表 1 顯示了與PerformanceTiming
對象中的每個屬性相對應的描述。chrome
PerformanceTiming
對象屬性對象屬性 | 描述 |
---|---|
navigationStart |
在導航開始的時候、在瀏覽器開始卸載前一頁(若是有這樣的頁面)的時候,或者在開始提取內容的時候,捕獲所需的數據。它將包含 unloadEventStart 數據或 fetchStart 數據。要想跟蹤端到端的時間,可從使用這個值開始。 |
unloadEventStart / unloadEventEnd |
在瀏覽器開始卸載前一頁或已完成前一頁的卸載的時候,捕獲所需的數據(若是相同域中有前一頁須要卸載的話)。 |
domainLookupStart / domainLookupEnd |
在瀏覽器開始和完成針對所請求內容的 DNS 查找時,捕獲所需的數據。 |
redirectStart /redirectEnd |
在瀏覽器開始和完成任何 HTTP 重定向時捕獲所需的數據。 |
connectStart /connectEnd |
在瀏覽器開始和完成創建當前頁面的遠程服務器 TCP 鏈接時捕獲所需的數據。 |
fetchStart |
在瀏覽器首次開始檢查用於所請求資源的緩存時捕獲所需的數據。 |
requestStart |
在瀏覽器經過發送 HTTP 請求來得到所請求的資源時捕獲所需的數據。 |
responseStart /responseEnd |
在瀏覽器首次進行註冊並完成註冊收到服務器響應時捕獲所需的數據。 |
domLoading /domComplete |
在文檔開始和完成加載時捕獲所需的數據。 |
domContentLoadedEventEnd /domContentLoadedEventStart |
在文檔的 DOMContentLoaded 開始和完成加載時捕獲所需的數據,這至關於瀏覽器已完成全部內容的加載並運行頁面中包含的全部腳本。 |
domInteractive |
在頁面的 Document.readyState 屬性變爲 interactive 時捕獲所需的數據,這會致使觸發 readystatechange 事件。 |
loadEventStart /loadEventEnd |
在加載事件觸發前和加載事件觸發後馬上捕獲所需的數據。 |
要想將上述步驟及其相應內容的順序更好地可視化,請參見圖 2。編程
PerformanceTiming
屬性的順序圖 3 顯示了包含已展開的 PerformanceNavigation
對象的 Performance
對象。json
PerformanceNavigation
對象請注意,導航對象有兩個只讀屬性:redirectCount
和 type
。顧名思義,redirectCount
屬性是 HTTP 重定向的數量,瀏覽器根據它們來獲取當前頁面。
HTTP 重定向是 Web 性能的一個重要因素,由於它們會致使每一次重定向都須要執行一次完整的 HTTP 往返過程。原始請求是從 Web 服務器返回的,做爲包含新位置路徑的 301 或 302。而後,瀏覽器必須初始化一個新的 TCP 鏈接,併發送一個新請求來得到新位置。這一附加步驟爲原始資源請求增長了額外的延遲。
redirectCount
屬性如清單 1 所示。
redirectCount
屬性>>> performance.navigation.redirectCount 0
導航對象的另外一個屬性是 type
。navigation.type
屬性是用下列常量表示的 4 個值中的一個:
TYPE_NAVIGATE
的值爲 0,表示可經過單擊一個連接、提交表單或直接在地址欄中輸入 URL 導航到當前頁面。TYPE_RELOAD
的值爲 1,表示經過重載操做到達當前頁面。TYPE_BACK_FORWARD
的值爲 2,表示經過使用瀏覽器歷史記錄、使用 back 或 forward 按鈕、以編程方式,或者經過瀏覽器的歷史對象來導航到頁面。TYPE_RESERVED
的值爲 255,它是其餘任何導航類型的全方位指示。要想使用這些對象來捕獲和可視化客戶端性能指標,可先建立一個 JavaScript 庫,收集 PerformanceTiming
數據,並將這些數據發送到某個端點進行收集和分析。查看這個 JavaScript 庫,該庫剛好用於完成這項工做。
perfLogger.js 腳本使用了 Performance
對象。建立一個名爲 perfLogger
的命名空間,並聲明一些局部變量來保存根據 PerformanceTiming
屬性推測的值。
您能夠經過使用這些示例和下面這些模式來計算時間:
timing.navigationStart
中減去當前時間。timing.redirectStart
中減去 timing.redirectEnd
。timing.domainLookupStart
中減去 timing.domainLookupEnd
,要想得到呈現頁面所用的時間,請從 xs
中減去當前時間。聲明並初始化局部變量後,使用 public getter 函數從命名空間暴露它們,如清單 2 所示。
getter
函數的局部變量var perfLogger = function(){ var serverLogURL = "/lib/savePerfData.php", loggerPool = [], _pTime = Date.now() - performance.timing .navigationStart || 0, _redirTime = performance.timing.redirectEnd - performance.timing.redirectStart || 0, _cacheTime = performance.timing.domainLookupStart - performance.timing.fetchStart || 0, _dnsTime = performance.timing.domainLookupEnd - performance.timing.domainLookupStart || 0, _tcpTime = performance.timing.connectEnd - performance.timing.connectStart || 0, _roundtripTime = performance.timing.responseEnd - performance.timing.connectStart || 0, _renderTime = Date.now() - performance.timing .domLoading || 0; //expose derived performance data perceivedTime: function(){ return _pTime; }, redirectTime: function(){ _redirTime; }, cacheTime: function(){ return _cacheTime; }, dnsLookupTime: function(){ return _dnsTime; }, tcpConnectionTime: function(){ return _tcpTime; }, roundTripTime: function(){ return _roundtripTime; }, pageRenderTime: function(){ return _renderTime; }, }
您能夠從命名空間訪問屬性,如清單 3 所示。
perfLogger.pageRenderTime
perfLogger. roundTripTime
perfLogger. tcpConnectionTime
perfLogger. dnsLookupTime
perfLogger. cacheTime
perfLogger. redirectTime
perfLogger. perceivedTime
在命名空間中,函數 logToServer
將指標從新寫回您在變量 serverLogURL
中定義的端點,如清單 4 所示。
logToServer
函數function logToServer(id){ var params = "data=" + JSON.stringify(jsonConcat (loggerPool[id],TestResults.prototype)); console.log(params) var xhr = new XMLHttpRequest(); xhr.open("POST", serverLogURL, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Content-length", params.length); xhr.setRequestHeader("Connection", "close"); xhr.onreadystatechange = function() { if (xhr.readyState==4 && xhr.status==200) { console.log('log written'); } }; xhr.send(params); }
perfLogger.js 庫還提供了一些基準測試功能,您能夠在其中測試 JavaScript 的專用數據塊,甚至能夠運行一組花費 N 時間量的測試來執行真正的基準測試。
perfLogger.js 庫的完整源代碼如清單 5 所示。
var perfLogger = function(){ var serverLogURL = "/lib/savePerfData.php", loggerPool = [], _pTime = Date.now() - performance.timing.navigationStart || 0, _redirTime = performance.timing.redirectEnd - performance.timing.redirectStart || 0, _cacheTime = performance.timing.domainLookupStart - performance.timing.fetchStart || 0, _dnsTime = performance.timing.domainLookupEnd - performance.timing.domainLookupStart || 0, _tcpTime = performance.timing.connectEnd - performance.timing.connectStart || 0, _roundtripTime = performance.timing.responseEnd - performance.timing.connectStart || 0, _renderTime = Date.now() - performance.timing.domLoading || 0; function TestResults(){}; TestResults.prototype.perceivedTime = _pTime; TestResults.prototype.redirectTime = _redirTime; TestResults.prototype.cacheTime = _cacheTime; TestResults.prototype.dnsLookupTime = _dnsTime; TestResults.prototype.tcpConnectionTime = _tcpTime; TestResults.prototype.roundTripTime = _roundtripTime; TestResults.prototype.pageRenderTime = _renderTime; function jsonConcat(object1, object2) { for (var key in object2) { object1[key] = object2[key]; } return object1; } function calculateResults(id){ loggerPool[id].runtime = loggerPool[id].stopTime - loggerPool[id].startTime; } function setResultsMetaData(id){ loggerPool[id].url = window.location.href; loggerPool[id].useragent = navigator.userAgent; } function drawToDebugScreen(id){ var debug = document.getElementById("debug") var output = formatDebugInfo(id) if(!debug){ var divTag = document.createElement("div"); divTag.id = "debug"; divTag.innerHTML = output document.body.appendChild(divTag); }else{ debug.innerHTML += output } } function logToServer(id){ var params = "data=" + JSON.stringify(jsonConcat( loggerPool[id],TestResults.prototype)); console.log(params) var xhr = new XMLHttpRequest(); xhr.open("POST", serverLogURL, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Content-length", params.length); xhr.setRequestHeader("Connection", "close"); xhr.onreadystatechange = function() { if (xhr.readyState==4 && xhr.status==200) { //console.log(xhr.responseText); } }; xhr.send(params); } function formatDebugInfo(id){ var debuginfo = "<p><strong>" + loggerPool[id].description + "</strong><br/>"; if(loggerPool[id].avgRunTime){ debuginfo += "average run time: " + loggerPool[id] .avgRunTime + "ms<br/>"; }else{ debuginfo += "run time: " + loggerPool[id].runtime + "ms<br/>"; } debuginfo += "path: " + loggerPool[id].url + "<br/>"; debuginfo += "useragent: " + loggerPool[id].useragent + "<br/>"; debuginfo += "Perceived Time: " + loggerPool[id].perceivedTime + "<br/>"; debuginfo += "Redirect Time: " + loggerPool[id].redirectTime + "<br/>"; debuginfo += "Cache Time: " + loggerPool[id].cacheTime + "<br/>"; debuginfo += "DNS Lookup Time: " + loggerPool[id].dnsLookupTime + "<br/>"; debuginfo += "tcp Connection Time: " + loggerPool[id].tcpConnectionTime + "<br/>"; debuginfo += "roundTripTime: "+ loggerPool[id].roundTripTime + "<br/>"; debuginfo += "pageRenderTime: " + loggerPool[id].pageRenderTime + "<br/>"; debuginfo += "</p>"; return debuginfo } return { startTimeLogging: function(id, descr,drawToPage ,logToServer){ loggerPool[id] = new TestResults(); loggerPool[id].id = id; loggerPool[id].startTime = performance.now(); loggerPool[id].description = descr; loggerPool[id].drawtopage = drawToPage; loggerPool[id].logtoserver = logToServer }, stopTimeLogging: function(id){ loggerPool[id].stopTime = performance.now(); calculateResults(id); setResultsMetaData(id); if(loggerPool[id].drawtopage){ drawToDebugScreen(id); } if(loggerPool[id].logtoserver){ logToServer(id); } }, logBenchmark: function(id, timestoIterate, func, debug, log){ var timeSum = 0; for(var x = 0; x < timestoIterate; x++){ perfLogger.startTimeLogging(id, "benchmarking "+ func, false, false); func(); perfLogger.stopTimeLogging(id) timeSum += loggerPool[id].runtime } loggerPool[id].avgRunTime = timeSum/timestoIterate if(debug){ drawToDebugScreen(id) } if(log){ logToServer(id) } }, //expose derived performance data perceivedTime: function(){ return _pTime; }, redirectTime: function(){ _redirTime; }, cacheTime: function(){ return _cacheTime; }, dnsLookupTime: function(){ return _dnsTime; }, tcpConnectionTime: function(){ return _tcpTime; }, roundTripTime: function(){ return _roundtripTime; }, pageRenderTime: function(){ return _renderTime; }, showPerformanceMetrics: function(){ this.startTimeLogging("no_id", "draw perf data to page" ,true,true); this.stopTimeLogging("no_id"); } } }(); performance.now = (function() { return performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow || function() { return new Date().getTime(); }; })();
要想使用 perfLogger.js 腳本可視化瀏覽器內的性能,能夠將該腳本嵌入頁面中,在頁面的 onload
事件上,您能夠將性能數據推送回端點,將它們保存爲一個平面文件。GitHub 中的 perfLogger
項目附帶了一個 PHP 腳本,名爲 savePerfData.php,該腳本剛好提供了此功能。該文件的源代碼如清單 6 所示。
<?php require("util/fileio.php"); $logfile = "log/runtimeperf_results.txt"; $benchmarkResults = formatResults($_POST["data"]); saveLog($benchmarkResults, $logfile); function formatResults($r){ print_r($r); $r = stripcslashes($r); $r = json_decode($r); if(json_last_error() > 0){ die("invalid json"); } return($r); } function formatNewLog($file){ $headerline = "IP, TestID, StartTime, StopTime, RunTime, URL, UserAgent, PerceivedLoadTime, PageRenderTime, RoundTripTime, TCPConnectionTime, DNSLookupTime, CacheTime, RedirectTime"; appendToFile($headerline, $file); } function saveLog($obj, $file){ if(!file_exists($file)){ formatNewLog($file); } $obj->useragent = cleanCommas($obj->useragent); $newLine = $_SERVER["REMOTE_ADDR"] . "," . $obj->id ."," . $obj->startTime . "," . $obj->stopTime . "," . $obj->runtime . "," . $obj->url . "," . $obj->useragent . $obj->perceivedTime . "," . $obj->pageRenderTime . "," . $obj->roundTripTime . "," . $obj->tcpConnectionTime . "," . $obj->dnsLookupTime . "," . $obj->cacheTime . "," . $obj->redirectTime; appendToFile($newLine, $file); } function cleanCommas($data){ return implode("", explode(",", $data)); } ?>
這個 PHP 實際上將 perfLogger.js 發送的 POST 數據保存爲一個平面文件,格式如清單 7 所示。
IP, TestID, StartTime, StopTime, RunTime, URL, UserAgent, PerceivedLoadTime, PageRenderTime, RoundTripTime, TCPConnectionTime, DNSLookupTime, CacheTime, RedirectTime 75.149.106.130,page_render,1341243219599,1341243220218,619 ,http://www.tom-barker.com/blog/?p=x,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.5; rv:13.0) Gecko/20100101 Firefox/13.0.1790,261,-2,36,0,-4,0
此時此刻,您能夠看到有一些很好的數據點值得關注,例如:
在 GitHub 存儲庫中,還有一個 R 腳本,名爲 runtimePerformance.R,該腳本將會攝取您生成的這個日誌文件,並實現數據可視化(參見清單 8)。
dataDirectory <- "/Applications/MAMP/htdocs/lab/log/" chartDirectory <- "/Applications/MAMP/htdocs/lab/charts/" testname = "page_render" perflogs <- read.table(paste(dataDirectory, "runtimeperf _results.csv", sep=""), header=TRUE, sep=",") perfchart <- paste(chartDirectory, "runtime_",testname, ". pdf", sep="") loadTimeDistrchart <- paste(chartDirectory, "loadtime_distribution.pdf", sep="") requestBreakdown <- paste(chartDirectory, "avgtime_inrequest.pdf", sep="") loadtime_bybrowser <- paste(chartDirectory, "loadtime_bybrowser.pdf", sep="") pagerender <- perflogs[perflogs$TestID == "page_render",] df <- data.frame(pagerender$UserAgent, pagerender$RunTime) df <- by(df$pagerender.RunTime, df$pagerender.UserAgent, mean) df <- df[order(df)] pdf(perfchart, width=10, height=10) opar <- par(no.readonly=TRUE) par(las=1, mar=c(10,10,10,10)) barplot(df, horiz=TRUE) par(opar) dev.off() getDFByBrowser<-function(data, browsername){ return(data[grep(browsername, data$UserAgent),]) } printLoadTimebyBrowser <- function(){ chrome <- getDFByBrowser(perflogs, "Chrome") firefox <- getDFByBrowser(perflogs, "Firefox") ie <- getDFByBrowser(perflogs, "MSIE") meanTimes <- data.frame(mean(chrome$PerceivedLoadTime), mean(firefox$PerceivedLoadTime), mean(ie$PerceivedLoadTime)) colnames(meanTimes) <- c("Chrome", "Firefox", "Internet Explorer") pdf(loadtime_bybrowser, width=10, height=10) barplot(as.matrix(meanTimes), main="Average Perceived Load Time\nBy Browser", ylim=c(0, 600), ylab="milliseconds") dev.off() } pdf(loadTimeDistrchart, width=10, height=10) hist(perflogs$PerceivedLoadTime, main="Distribution of Perceived Load Time", xlab="Perceived Load Time in Milliseconds", col=c("#CCCCCC")) dev.off() avgTimeBreakdownInRequest <- function(){ #expand exponential notation options(scipen=100, digits=3) #set any negative values to 0 perflogs$PageRenderTime[perflogs$PageRenderTime < 0] <- 0 perflogs$RoundTripTime[perflogs$RoundTripTime < 0] <- 0 perflogs$TCPConnectionTime[perflogs$TCPConnectionTime < 0] <- 0 perflogs$DNSLookupTime[perflogs$DNSLookupTime < 0] <- 0 #capture avg times avgTimes <- data.frame(mean(perflogs$PageRenderTime), mean(perflogs$RoundTripTime), mean(perflogs$TCPConnectionTime), mean(perflogs$DNSLookupTime)) colnames(avgTimes) <- c("PageRenderTime", "RoundTripTime", "TCPConnectionTime", "DNSLookupTime") pdf(requestBreakdown, width=10, height=10) opar <- par(no.readonly=TRUE) par(las=1, mar=c(10,10,10,10)) barplot(as.matrix(avgTimes), horiz=TRUE, main="Average Time Spent\nDuring HTTP Request", xlab="Milliseconds") par(opar) dev.off() } printLoadTimebyBrowser() avgTimeBreakdownInRequest()
這個 R 腳本附帶了一些內置的功能,例如 printLoadTimebyBrowser
和 avgTimeBreakdownInRequest
。圖 4 是 printLoadTimebyBrowser
輸出的屏幕截圖。
printLoadTimebyBrowser
的輸出圖 5 是 avgTimeBreakdownInRequest
的屏幕截圖。
avgTimeBreakdownInRequest
代碼的輸出code在將性能數據加載到 R 會話中以後,全部已攝取的數據指標都存儲在一個名爲 perflogs
的數據幀內,這樣您就能夠訪問單獨的列,如清單 9 所示。
perflogs$PerceivedLoadTime
perflogs$ PageRenderTime
perflogs$RoundTripTime
perflogs$TCPConnectionTime
perflogs$DNSLookupTime
perflogs$UserAgent
該代碼支持您開始執行一些探索性的數據分析,好比建立柱狀圖來查看用戶羣的感知加載時間的分佈,如清單 10 所示。
hist(perflogs$PerceivedLoadTime, main="Distribution of Perceived Load Time", xlab="Perceived Load Time in Milliseconds", col=c("#CCCCCC")) dev.off()
圖 6 顯示了用戶羣的感知加載時間分佈柱狀圖。
本文幫助您更好地瞭解了 Performance
對象中一些功能,並模擬瞭如何使用可從該對象收集的瀏覽器內指標。經過這裏介紹的模型,您能夠從實際用戶羣中捕獲真正的用戶指標,這是您能夠收集並跟蹤的最有價值的性能指標類型。
若是願意的話,您還可使用該模型中的 perfLogger.js 和全部實用程序文件。您能夠隨時發表您本身的意見以及對該項目的更改。