当考虑 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 和全部实用程序文件。您能够随时发表您本身的意见以及对该项目的更改。