前端页面性能指标数据计算方法

上面我们梳理了页面性能和体验相关的数据指标,聪明的你可能会想到怎么获取这些指标对应的数据呢?别着急,事实上Web Performance Working Group 制定了很多相关的接口标准,部分已经实现,部分还在草案阶段。我们主要用到Performance这个接口,它提供的数据基本包含了我们需要的各个指标。

指标数据采集API

Performance接口允许访问当前页面性能相关的信息。它是High Resolution Time API的一部分。但是它被Performance Timeline API, the Navigation Timing API, the User Timing API, and the Resource Timing API 扩展增强了,实际上Performance的主要功能都是由这几个API提供的。我们分别介绍下:

High Resolution Time API

这里必须要介绍下 High Resolution Time API,主要定义了 DOMHighResTimeStamp 数据类型,这是一个高精度时间戳类型,double类型,相对于 time originglobal monotonic clock的时间。或者是两个高精度时间戳的时间差值。其单位是毫秒。如果由于设备软件或者硬件的限制,浏览器不能提供5微秒精度的时间,那么浏览器提供的DOMHighResTimeStamp时间至少精确到1ms,防止时序攻击performance.now提供了一个相对于time-origin的精确时间间隔。

performance.now()

973141.500000027

Navigation Timing API

Navigation Timing标准中介绍到这个API主要包含两个接口:PerformanceTimingPerformanceNavigation,这两个接口由浏览器进行实现和维护,当浏览器创建页面的时候就会把接口定义的相关数据挂载到window.performance.timing和window.performance.navigation这两个属性上。可以参考下一节 加载链路优化 这部分内容。

其中PerformanceTiming的时间点属性有这些:

interface PerformanceTiming {
  readonly attribute unsigned long long navigationStart;
  readonly attribute unsigned long long unloadEventStart;
  readonly attribute unsigned long long unloadEventEnd;
  readonly attribute unsigned long long redirectStart;
  readonly attribute unsigned long long redirectEnd;
  readonly attribute unsigned long long fetchStart;
  readonly attribute unsigned long long domainLookupStart;
  readonly attribute unsigned long long domainLookupEnd;
  readonly attribute unsigned long long connectStart;
  readonly attribute unsigned long long connectEnd;
  readonly attribute unsigned long long secureConnectionStart;
  readonly attribute unsigned long long requestStart;
  readonly attribute unsigned long long responseStart;
  readonly attribute unsigned long long responseEnd;
  readonly attribute unsigned long long domLoading;
  readonly attribute unsigned long long domInteractive;
  readonly attribute unsigned long long domContentLoadedEventStart;
  readonly attribute unsigned long long domContentLoadedEventEnd;
  readonly attribute unsigned long long domComplete;
  readonly attribute unsigned long long loadEventStart;
  readonly attribute unsigned long long loadEventEnd;
};

简单介绍几个有用的时间点:

  • startTime:有些浏览器实现为navigationStart,代表浏览器开始unload前一个页面文档的开始时间节点。比如我们当前正在浏览baidu.com,在地址栏输入google.com并回车,浏览器的执行动作依次为:unload当前文档(即baidu.com)->请求下一文档(即google.com)。navigationStart的值便是触发unload当前文档的时间节点。

    如果当前文档为空,则navigationStart的值等于fetchStart。

  • redirectStartredirectEnd:如果页面是由redirect而来,则redirectStart和redirectEnd分别代表redirect开始和结束的时间节点;
  • unloadEventStartunloadEventEnd:如果前一个文档和请求的文档是同一个域的,则unloadEventStartunloadEventEnd分别代表浏览器unload前一个文档的开始和结束时间节点。否则两者都等于0;
  • fetchStart是指在浏览器发起任何请求之前的时间值。在fetchStart和domainLookupStart之间,浏览器会检查当前文档的缓存APP Cache。

这个App Cache(应用缓存)主要用来提供离线浏览网页的功能,不做在新的浏览器标准中已经移除了,不推荐使用,新的标准推荐采用Service Worker方案来处理离线数据缓存,参考MDN  whatwg

  • domainLookupStartdomainLookupEnd分别代表DNS查询的开始和结束时间节点。如果浏览器没有进行DNS查询(比如使用了DNS cache),则两者的值都等于fetchStart
  • connectStartconnectEnd分别代表TCP建立连接和连接成功的时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接webscoket),则两者都等于domainLookupEnd
  • secureConnectionStart:可选。如果页面使用HTTPS,它的值是安全连接握手之前的时刻。如果该属性不可用,则返回undefined。如果该属性可用,但没有使用HTTPS,则返回0;
  • requestStart代表浏览器发起请求的时间节点,请求的方式可以是请求服务器、缓存、本地资源等;
  • responseStartresponseEnd分别代表浏览器收到从服务器端(或缓存、本地资源)响应回的第一个字节和最后一个字节数据的时刻;
  • domLoading代表浏览器开始解析html文档的时间节点。我们知道IE浏览器下的document有readyState属性,domLoading的值就等于readyState改变为loading的时间节点,此时document.readyState = loading
  • domInteractive代表浏览器解析html文档的状态为interactive时的时间节点。domInteractive并非DOMReady,它早于DOMReady触发,代表html文档解析完毕(即dom tree创建完成)但是内嵌资源(比如外链css、js等)还未加载的时间点;此时document.readyState = interactive
  • domContentLoadedEventStart:代表DOMContentLoaded事件触发的时间节点:

    页面文档完全加载并解析完毕之后,会触发DOMContentLoaded事件,HTML文档不会等待样式文件,图片文件,子框架页面的加载(load事件可以用来检测HTML页面是否完全加载完毕(fully-loaded))。

  • domContentLoadedEventEnd:代表DOMContentLoaded事件完成的时间节点,此刻用户可以对页面进行操作,也就是jQuery中的domready时间;
  • domComplete:html文档完全解析完毕的时间节点, 此时document.readyState = complete
  • loadEventStartloadEventEnd分别代表onload事件触发和结束的时间节点

使用该api时需要在页面完全加载完成之后才能使用,最简单的办法是在window.onload事件中读取各种数据,因为很多值必须在页面完全加载之后才能得出。我们需要采集的TTFB和秒开率数据指标 可以从这个API里面拿到,示例如下:

var timing = window.performance && window.performance.timing;
var navigation = window.performance && window.performance.navigation;

// TTFB 数据 
timing.responseStart - timing.fetchStart 

// TTFB to DOM Ready, 暂不采集 
timing.domInteractive - timing.fetchStart 

// Page Load页面打开时间,作为秒开率指标。 
timing.loadEventStart - timing.fetchStart
 
 
重定向次数:
var redirectCount = navigation && navigation.redirectCount;
 
跳转耗时:
var redirect = timing.redirectEnd - timing.redirectStart;
 
APP CACHE 耗时:
var appcache = Math.max(timing.domainLookupStart - timing.fetchStart, 0);
 
DNS 解析耗时:
var dns = timing.domainLookupEnd - timing.domainLookupStart;
 
TCP 链接耗时:
var conn = timing.connectEnd - timing.connectStart;
 
等待服务器响应耗时(注意是否存在cache):
var request = timing.responseStart - timing.requestStart;
 
内容加载耗时(注意是否存在cache):
var response = timing.responseEnd - timing.responseStart;
 
总体网络交互耗时,即开始跳转到服务器资源下载完成:
var network = timing.responseEnd - timing.navigationStart;
 
渲染处理:
var processing = (timing.domComplete || timing.domLoading) - timing.domLoading;
 
抛出 load 事件:
var load = timing.loadEventEnd - timing.loadEventStart;
 
总耗时:
var total = (timing.loadEventEnd || timing.loadEventStart || timing.domComplete || timing.domLoading) - timing.navigationStart;
 
可交互:
var active = timing.domInteractive - timing.navigationStart;
 
请求响应耗时,即 T0,注意cache:
var t0 = timing.responseStart - timing.navigationStart;
 
首次出现内容,即 T1:
var t1 = timing.domLoading - timing.navigationStart;
 
内容加载完毕,即 T3:
var t3 = timing.loadEventEnd - timing.navigationStart;


PS:注意这个timing中返回的数据单位精度是毫秒。我们尝试统计下当前页面的时间:

;
(function() {
    function getTiming(){
        try {
            var time = performance.timing;
            var timingObj = {};

            var loadTime = (time.loadEventEnd - time.loadEventStart) / 1000;

            if(loadTime < 0) {
                setTimeout(function() {
                    getTiming();
                }, 200);
                return;
            }

            timingObj['重定向时间'] = (time.redirectEnd - time.redirectStart) / 1000;
            timingObj['DNS解析时间'] = (time.domainLookupEnd - time.domainLookupStart) / 1000;
            timingObj['APP缓存时间'] = (Math.max(time.domainLookupStart - time.fetchStart, 0)) / 1000;
            timingObj['TCP完成握手时间'] = (time.connectEnd - time.connectStart) / 1000;
            timingObj['HTTP请求响应完成时间'] = (time.responseEnd - time.requestStart) / 1000;
            timingObj['DOM开始加载前所花费时间'] = (time.responseEnd - time.navigationStart) / 1000;
            timingObj['DOM加载完成时间'] = (time.domComplete - time.domLoading) / 1000;
            timingObj['DOM结构解析完成时间'] = (time.domInteractive - time.domLoading) / 1000;
            timingObj['脚本加载时间'] = (time.domContentLoadedEventEnd - time.domContentLoadedEventStart) / 1000;
            timingObj['onload事件时间'] = (time.loadEventEnd - time.loadEventStart) / 1000;
            timingObj['页面完全加载时间'] = (timingObj['重定向时间'] + timingObj['DNS解析时间'] + timingObj['TCP完成握手时间'] + timingObj['HTTP请求响应完成时间'] + timingObj['DOM结构解析完成时间'] + timingObj['DOM加载完成时间']);

            for(item in timingObj) {
                console.log(item + ":" + timingObj[item] + '秒(s)');
            }

        } catch(e) {
            console.log(timingObj)
            console.log(performance.timing);
        }
    }
    getTiming()
    
})();

 

User Timing API

User Timing 接口提供了很多方法,让我们可以在应用中的不同位置去调用这些方法,计算用户自己需要的时间。常用的有mark、measure和clearMarks方法。

mark() 方法是时间分析工具中的主要方法,它的功能就是为我们记录时间,其超级有用之处在于我们可以为我们记录的时间命名,它会将这个名字和时间作为一个独立的单元来记住。在应用中不同位置调用 mark() 方法可以让你知道应用中被标记的位置所花费的时间。

measure() 方法不仅可以计算设置标志之间的时间,而且也能计算标志和 PerformanceTiming 接口中那些已知的事件名之间的时间。

clearMarks() 方法可以很简单滴来清除标志。clearMeasures方法可以很简单滴来清除measure。

给个简单的例子:

window.performance.mark('mark_fully_loaded');

var perfEntries = performance.getEntriesByType("mark");
  for (var i = 0; i < perfEntries.length; i++) {
    console.log("Name: " + perfEntries[i].name +
      " Entry Type: " + perfEntries[i].entryType +
      " Start Time: " + perfEntries[i].startTime +
      " Duration: "   + perfEntries[i].duration  + "\n");
  }
//output  Name: mark_fully_loaded Entry Type: mark Start Time: 9910.900000017136 Duration: 0

window.performance.measure('measure_load_from_dom', 'domComplete', 'mark_fully_loaded');
var perfEntries = performance.getEntriesByType("measure");
  for (var i = 0; i < perfEntries.length; i++) {
    console.log("Name: " + perfEntries[i].name +
      " Entry Type: " + perfEntries[i].entryType +
      " Start Time: " + perfEntries[i].startTime +
      " Duration: "   + perfEntries[i].duration  + "\n");
  }
//output  Name: measure_load_from_dom Entry Type: measure Start Time: 8643 Duration: 1267.9000000171363

window.performance.clearMarks();
window.peformance.clearMarks('mark_fully_loaded');
window.performance.clearMeasures('measure_load_from_dom');

// AJAX请求时间
var reqCount = 0;
var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
  window.performance.mark('mark_end_xhr');
  reqCnt++;
  window.performance.measure('measure_xhr_' + reqCnt, 'mark_start_xhr', 'mark_end_xhr');
  do_something(e.responseText);
}
window.performance.mark('mark_start_xhr');
myReq.send();
// 计算时间
var items = window.performance.getEntriesByType('measure');
for (var i = 0; i < items.length; ++i) {
  var req = items[i];
  console.log('XHR ' + req.name + ' took ' + req.duration + 'ms');
}

这个很有用的API经常拿来计算FMP指标。前面也说了,很难统一定义FMP计算规则,不同业务不一样。本课程第一节的时候,我就要求大家一定要先了解业务,了解用户。其中提到了要了解自己做的业务的核心是什么,页面展示的核心是什么。FMP主要是衡量页面核心元素的展示时间。举个例子,如果你做的是视频播放页面,那么加载完播放器到可播放状态就是FMP。给出一个例子:

// 测量 css 加载完成时间:
<link rel="stylesheet" href="/sheet1.css">
<link rel="stylesheet" href="/sheet4.css">
<script> performance.mark("stylesheets done blocking"); </script>

// 测量关键图片加载完成时间:
<img src="hero.jpg" onload="performance.clearMarks('img displayed'); performance.mark('img displayed');">
<script>
performance.clearMarks("img displayed");
performance.mark("img displayed");
</script>

// 测量文字类元素加载完成时间:
<p>This is the call to action text element.</p>
<script>  performance.mark("text displayed"); </script>

// 计算加载时间:
function measurePerf() {
  var perfEntries = performance.getEntriesByType("mark");
  for (var i = 0; i < perfEntries.length; i++) {
    console.log("Name: " + perfEntries[i].name +
      " Entry Type: " + perfEntries[i].entryType +
      " Start Time: " + perfEntries[i].startTime +
      " Duration: "   + perfEntries[i].duration  + "\n");
  }
}

主要用mark标记所有需要统计的时间点,然后拿到这些数据上报。

Resource Timing API

主要是当前浏览器获取所有资源的API,主要是这个PerformanceResourceTiming 接口,entryType是”resource”。

interface PerformanceResourceTiming : PerformanceEntry {
    readonly attribute DOMString           initiatorType;
    readonly attribute DOMHighResTimeStamp redirectStart;
    readonly attribute DOMHighResTimeStamp redirectEnd;
    readonly attribute DOMHighResTimeStamp fetchStart;
    readonly attribute DOMHighResTimeStamp domainLookupStart;
    readonly attribute DOMHighResTimeStamp domainLookupEnd;
    readonly attribute DOMHighResTimeStamp connectStart;
    readonly attribute DOMHighResTimeStamp connectEnd;
    readonly attribute DOMHighResTimeStamp secureConnectionStart;
    readonly attribute DOMHighResTimeStamp requestStart;
    readonly attribute DOMHighResTimeStamp responseStart;
    readonly attribute DOMHighResTimeStamp responseEnd;
    serializer = {inherit, attribute};
};

获取performance所有记录时,会看到返回的有PerformanceResourceTiming的数据(entryType为 “resource”)。其中 initiatorType 是请求发起的类型,这里有外链CSS中的link标签,img标签,script标签,CSS(@import 导入的)等等。

Performance Timeline API

顾名思义,这个 Timeline API 用于列出页面的时间线,包含用户自定义的时间点和PerformanceTiming默认的时间点。主要是扩展了performance接口,增加了 getEntries、getEntriesByType、getEntriesByName这三个方法,返回 PerformanceEntry 对象,定义如下:

[Exposed=(Window,Worker)]
interface PerformanceEntry {
  readonly    attribute DOMString name;
  readonly    attribute DOMString entryType;
  readonly    attribute DOMHighResTimeStamp startTime;
  readonly    attribute DOMHighResTimeStamp duration;
  [Default] object toJSON();
};

为了优化timeline数据的获取和处理,增加了PerformanceObserver接口。用法对比如下:

// 旧的写法
function init() {
      // see [[USER-TIMING-2]]
      performance.mark("startWork");
      doWork(); // Some developer code
      performance.mark("endWork");
      measurePerf();
}
function measurePerf() {
  performance
    .getEntries()
    .map(entry => JSON.stringify(entry, null, 2))
    .forEach(json => console.log(json));
}

// 基于PerformanceObserver新用法
const observer = new PerformanceObserver(list => {
  list
    .getEntries()
    // Get the values we are interested in
    .map(({ name, entryType, startTime, duration }) => {
      const obj = {
        "Duration": duration,
        "Entry Type": entryType,
        "Name": name,
        "Start Time": startTime,
      };
      return JSON.stringify(obj, null, 2);
    })
    // Display them to the console
    .forEach(console.log);
  // maybe disconnect after processing the events.
  observer.disconnect();
});
// retrieve buffered events and subscribe to new events
// for Resource-Timing and User-Timing
observer.observe({
  entryTypes: ["resource", "mark", "measure"],
  buffered: true
});

FP/FCP、卡顿(longtask)指标

可以通过获取paint类型的条目来计算FP/FCP指标,如下:

可以通过PerformanceObserver获取long task数据,进而得到卡顿指标。

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log( {
      eventCategory: 'Performance Metrics',
      eventAction: 'longtask',
      eventValue: Math.round(entry.startTime + entry.duration),
      eventLabel: JSON.stringify(entry.attribution),
    });
  }
});

observer.observe({entryTypes: ['longtask']});

 

内存堆栈数据

直接通过performance这个对象可以拿到,挂载在memory这个属性下,如下:

 

TTI指标

TTI指标采集可以采用谷歌提供的 tti-polyfill。使用方法:

npm install tti-polyfill

import ttiPolyfill from './path/to/tti-polyfill.js';

ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
  // Use `tti` value in some way.
});

交互延迟指标

通用的处理方法,可以用MutationObserver来监听DOM变化,用performance.mark来统计用户交互操作后到响应的时间。以统计无效点击为例:

let targetNode = document.body;

// Options for the observer (which mutations to observe)
let config = {
    attributes: true,
    childList: true,
    subtree: true
};

// Callback function to execute when mutations are observed
const mutationCallback = (callback) => { 
  setTimeout(callback, 150)
  return (mutationsList) => {

    for(let mutation of mutationsList) {
        let type = mutation.type;
        switch (type) {
            case "childList": 
            case "attributes":
            case "subtree":
                callback && callback();
                break;
            default:
                break;
        }
    }
  }
};



// 点击事件
targetNode.addEventListener("click",function(){
    performance.clearMarks("click_start");
    performance.mark("click_start");

    // Create an observer instance linked to the callback function
    let observer = new MutationObserver(mutationCallback(function(){
        performance.clearMarks("click_end");
        performance.mark("click_end");
        
        window.performance.measure('click_res_time', 'click_start', 'click_end');
        // 在统一监听PerformanceObserver的地方可以拿到这个点击后响应的时间,进行上报
        // Later, you can stop observing
        observer.disconnect();
     }));

    // Start observing the target node for configured mutations
    observer.observe(targetNode, config);
}, true)


// 获取,这里只是示例,可以用上面的performanceObserver
var perfEntries = performance.getEntriesByType("measure");
  for (var i = 0; i < perfEntries.length; i++) {
    console.log("Name: " + perfEntries[i].name +
      " Entry Type: " + perfEntries[i].entryType +
      " Start Time: " + perfEntries[i].startTime +
      " Duration: "   + perfEntries[i].duration  + "\n");
  }

EventTiming API

这里必须介绍下Event Timing API,有了这个API,我们统计交互延迟数据方便很多,主要用来监听响应超过100ms的示例:

const observer = new PerformanceObserver(function(list) {
        const perfEntries = list.getEntries();
        for (let i = 0; i < perfEntries.length; i++) {
            // Process event and report to analytics and monitoring...
            const entry = perfEntries[i];
            const inputDelay = entry.processingStart - entry.startTime;
            if (inputDelay !== 0) {
                // Report the input delay when there are event handlers.
            }
        }
    });
    // Register observer for event.
    observer.observe({entryTypes: ["event"]});
    ...
    // Later on, we can also directly query the first input information.
    const firstArray = performance.getEntriesByType('firstInput');
    if (firstArray.length !== 0) {
        const firstInput = firstArray[0];
        // Process the first input event and report back...
    }

通过Event Timing API可以很容易拿到FID时间。event timing entry的定义如下:

interface PerformanceEventTiming : PerformanceEntry {
    readonly attribute DOMHighResTimeStamp processingStart;
    readonly attribute DOMHighResTimeStamp processingEnd;
    readonly attribute boolean cancelable;
};

PerformanceEventTiming 继承了 PerformanceEntry 接口,PerformanceEntry 接口包含下面4个属性:

name相关事件的类型
entryType:对于比较长的耗时时间,返回 "event",首次用户交互返回 "firstInput"
startTime:事件开始的timeStamp.
duration:The duration attribute’s getter must return the difference between the time of the first update the rendering occurring after associated event has been dispatched and the startTime, rounded up to the nearest 8 ms.

PerformanceEventTiming 有下面额外的3个属性:

processingStart:The processingStart attribute’s getter returns the time when event handlers start to execute, or startTime if there are no event handlers.
processingEnd:The processingEnd attribute’s getter returns the time when event handlers have finished executing, or startTime if there are no event handlers.
cancelable:The cancelable attribute’s getter must return the associated event’s cancelable.

FID时间

关于FID官方给了一个实现库,其实我们用上面的代码已经计算了FID。官方库:first-input-delay 使用方法:

npm install --save-dev first-input-delay
// The perfMetrics object is created by the code that goes in <head>.
perfMetrics.onFirstInputDelay(function(delay, evt) {
  ga('send', 'event', {
    eventCategory: 'Perf Metrics',
    eventAction: 'first-input-delay',
    eventLabel: evt.type,
    // Event values must be an integer.
    eventValue: Math.round(delay),
    // Exclude this event from bounce rate calculations.
    nonInteraction: true,
  });
});

 

OriginTrials

我们一般在about:flags里打开了“Experimental Web Platform features”,这个开关使得我的Chrome开启了该实验特性,而我们的用户并不会打开。针对这个问题,实际上Chrome有一个“Origin Trials”的特性,通过该特性Web开发者可以主动申明在自己的站点上启用该特性,而不需要用户手动开启,对于Web开发者来说我们可以更早地使用新特性,而对于浏览器及Web标准组织来说也可以收到更多的使用反馈。官方使用手册:guide

注册token

首先你要注册一个token,设置你想要开启的域名,然后会生成一个token。

配置页面

有两种方式

  1. 页面添加meta标签
<meta http-equiv="origin-trial" content="**insert your token as provided in the developer console**">
  1. 页面的http 响应header返回下面的头部
Origin-Trial: **token as provided in the developer console**

更多信息,看下官方说明

 

采样

说一个采样的概念,我们不能把所有客户端的数据都采集回来,当你的业务每天有几百万的pv的时候,数据量是可怕的。而且我们也不需要这么多的数据,所以我们只需要采集部分客户端的数据就可以了。这就是采样的概念。要先确定一些指标:

  • 采集多少量
  • 采样规则

 

总结与思考

本章我们从高性能、极致用户体验的目的出发,总结了加载性能指标、稳定性指标、用户体验指标 三个大的指标集,作为后面我们分析页面性能的依据,验证我们开发的页面是否达到了要求。然后我们研究了浏览器提供的Performance API,利用浏览器提供的统计信息来计算我们的性能和体验指标。

知识共享署名4.0国际许可协议,转载请保留出处; 部分内容来自网络,若有侵权请联系我:前端学堂 » 前端页面性能指标数据计算方法

赞 (1) 打赏

评论 0

如果对您有帮助,别忘了打赏一下宝宝哦!

支付宝扫一扫打赏

微信扫一扫打赏