前端性能监控指标

主要指标

白屏时间

白屏时间(first paint time)是用户打开页面到首次看到内容的时间,也叫做首次渲染时间。白屏时间出现在头部外链资源加载完附近,因为浏览器只有加载并解析完头部资源才会真正渲染页面。基于此我们可以通过获取头部资源加载完的时刻来近似统计白屏时间。尽管并不精确,但却考虑了影响白屏的主要因素:首字节时间和头部资源加载时间。

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8"/>
    <script>
      var start_time = +new Date; //测试时间起点,实际统计起点为 DNS 查询
    </script>
    <!-- 3s 后这个 js 才会返回 -->
    <script src="script.php"></script> 
    <script>
      var end_time = +new Date; //时间终点
      var headtime = end_time - start_time; //头部资源加载时间   
      console.log(headtime);
    </script>
    </head>
    <body>    
    <p>在头部资源加载完之前页面将是白屏</p>
    <p>script.php 被模拟设置 3s 后返回,head 底部内嵌 JS 等待前面 js 返回后才执行</p>
    <p>script.php 替换成一个执行长时间循环的 js 效果也一样</p> 
    </body>
</html>

上述代码中end_time和start_time的差值即可作为页面的白屏时间

首屏时间

首屏时间的统计比较复杂,目前应用比较广的方案是将首屏的图片、iframe等资源添加onload事件,获取最慢的一个。

首屏位置调用 API 开始统计 -> 绑定首屏内所有图片的 load 事件 -> 页面加载完后判断图片是否在首屏内,找出加载最慢的一张 -> 首屏时间

这是同步加载情况下的简单统计逻辑,另外需要注意的几点:
– 页面存在 iframe 的情况下也需要判断加载时间
– gif 图片在 IE 上可能重复触发 load 事件需排除
– 异步渲染的情况下应在异步获取数据插入之后再计算首屏
– css 重要背景图片可以通过 JS 请求图片 url 来统计(浏览器不会重复加载)
– 没有图片则以统计 JS 执行时间为首屏,即认为文字出现时间

 // 首屏时间计算脚本示例
function first_screen_time() {
    var imgs = document.getElementsByTagName("img");
    var iframes = document.getElementsByTagName("IFRAME");
    var timestamp = +new Date;
    var imgtime = [];
    var that = this;

    function offsetTop(i) {
        var top = 0;
        top = window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop;
        try {
            top += i.getBoundingClientRect().top;
        } catch (k) {} finally {
            return top;
        }
    }

    var handler = function() {
        if (this.removeEventListener) {
            this.removeEventListener("load", handler, false)
        }
        imgtime.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", handler, false);
            } else {
                if (img.attachEvent) {
                    img.attachEvent("onreadystatechange", function() {
                        if (img.readyState == "complete") {
                            handler.call(img, handler);
                        }
                    })
                }
            }
        })()
    }
    for (var i = 0, d = iframes.length; i < d; i++) {
        (function() {
            var iframe = iframes[i];
            if (iframe.attachEvent) {
                iframe.attachEvent("onload", function() {
                    handler.call(iframe, handler)
                });
            } else {
                iframe.addEventListener("load", handler, false)
            }
        })()
    }
    return function(fsHeight) {
        var fsHeight = fsHeight || document.documentElement.clientHeight;
        for (var i = 0; i < imgtime.length; i++) {
            var item = imgtime[i];
            var dom = item.img;
            var time = item.time;
            var offsetTop = offsetTop(dom);
            if (offsetTop > 0 && offsetTop < fsHeight) {
                timestamp = time > timestamp ? time : timestamp
            }
        }
        return timestamp;
    };
}

用户可操作时间

用户可操作默认可以统计domready时间,因为通常会在这时候绑定事件操作。对于使用了模块化异步加载的 JS 可以在代码中去主动标记重要 JS 的加载时间,这也是产品指标的统计方式。

但在实际项目中,通过domready来作为用户可操作时间显示是不准确的,比如项目中使用了第三方的类库框架开发,用户可操作时间点可能就是在,框架加载完成并且将试图渲染并绑定时间完成时作为用户可操作时间,是一个更加准确的时间。

var ready = (function (callback) {
    var isReady = false;
    var readyList = [];
    // The ready event handler
    var DOMContentLoaded = function( event ) {
        // readyState === "complete" is good enough for us to call the dom ready in oldIE
        if ( document.addEventListener || event.type === 'load' || document.readyState === 'complete' ) {
            detach();
            ready();
        }
    };
    // Clean-up method for dom ready events
    var detach = function() {
        if ( document.addEventListener ) {
            document.removeEventListener('DOMContentLoaded', DOMContentLoaded, false);
            window.removeEventListener('load', DOMContentLoaded, false);
        } else {
            document.detachEvent('onreadystatechange', DOMContentLoaded);
            window.detachEvent('onload', DOMContentLoaded);
        }
    };
    // Handle when the DOM is ready
    function ready() {
        if(!isReady) {
            isReady = true;
            for (var i = 0, j = readyList.length; i < j; i++) {
                readyList[i]();
            }
        }
    }
    // Catch cases where $(document).ready() is called after the browser event has already occurred.
    // we once tried to use readyState "interactive" here, but it caused issues like the one
    // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
    if ( document.readyState === "complete" ) {
        // Handle it asynchronously to allow scripts the opportunity to delay ready
        setTimeout( ready );
    // Standards-based browsers support DOMContentLoaded
    }else if (document.addEventListener) {
        // Use the handy event callback
        document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);
        // A fallback to window.onload, that will always work
        window.addEventListener('load', ready, false);
    // If IE event model is used
    } else {
        // Ensure firing before onload, maybe late but safe also for iframes
        document.attachEvent('onreadystatechange', DOMContentLoaded);
        // A fallback to window.onload, that will always work
        window.attachEvent('onload', ready);
        // If IE and not a frame
        // continually check to see if the document is ready
        var top = false;
        try {
            top = window.frameElement == null && document.documentElement;
        } catch(e) {}
        if (top && top.doScroll) {
            (function() {
                if (!isReady) {
                    try {
                        // Use the trick by Diego Perini
                        // http://javascript.nwbox.com/IEContentLoaded/
                        top.doScroll("left");
                    } catch (e) {
                        return setTimeout(arguments.callee, 1);
                    }
                    // and execute any waiting functions
                    ready();
                }
            })();
        }
    }
    return function(callback) {
        isReady ? callback() : readyList.push(callback)
    }
    // end
})();

总下载时间

总下载时间默认可以统计onload时间,这样可以统计同步加载的资源全部加载完的耗时。如果页面中存在很多异步渲染,可以将异步渲染全部完成的时间作为总下载时间。

var onloader = function(callback) {
    var loaded = function(callback) {
        if ( document.addEventListener ) {
            window.removeEventListener('load', loaded, false );
        } else {
            window.detachEvent('onload', loaded );
        }
        callback();
    };
    if (document.addEventListener) {
        window.addEventListener('load', loaded, false);
    } else {
        window.attachEvent('onload', loaded);
    }
};

网络指标

window.performance 对象

performance 说明
memory javascript内存占用情况
navigation 网页导航相关信息
timing { 包含网络、解析等一系列的时间数据
connectEnd 分别代表TCP建立连接和连接成功的时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接webscoket),则两者都等于domainLookupEnd;
connectStart
domComplete html文档完全解析完毕的时间节点;
domContentLoadedEventEnd 代表DOMContentLoaded事件触发的时间节点:页面文档完全加载并解析完毕之后,会触发DOMContentLoaded事件,HTML文档不会等待样式文件,图片文件,子框架页面的加载(load事件可以用来检测HTML页面是否完全加载完毕(fully-loaded))。
domContentLoadedEventStart 代表DOMContentLoaded事件完成的时间节点,此刻用户可以对页面进行操作,也就是jQuery中的domready时间;
domInteractive 代表浏览器解析html文档的状态为interactive时的时间节点。domInteractive并非DOMReady,它早于DOMReady触发,代表html文档解析完毕(即dom tree创建完成)但是内嵌资源(比如外链css、js等)还未加载的时间点;
domLoading 代表浏览器开始解析html文档的时间节点。我们知道IE浏览器下的document有readyState属性,domLoading的值就等于readyState改变为loading的时间节点;
domainLookupEnd 分别代表DNS查询的开始和结束时间节点。如果浏览器没有进行DNS查询(比如使用了cache),则两者的值都等于fetchStart;
domainLookupStart
fetchStart 是指在浏览器发起任何请求之前的时间值。在fetchStart和domainLookupStart之间,浏览器会检查当前文档的缓存;
loadEventEnd 分别代表onload事件触发和结束的时间节点
loadEventStart
navigationStart
redirectEnd 如果页面是由redirect而来,则redirectStart和redirectEnd分别代表redirect开始和结束的时间节点;
redirectStart
requestStart 代表浏览器发起请求的时间节点,请求的方式可以是请求服务器、缓存、本地资源等;
responseEnd 分别代表浏览器收到从服务器端(或缓存、本地资源)响应回的第一个字节和最后一个字节数据的时刻;
responseStart
secureConnectionStart 可选。如果页面使用HTTPS,它的值是安全连接握手之前的时刻。如果该属性不可用,则返回undefined。如果该属性可用,但没有使用HTTPS,则返回0;
unloadEventEnd 如果前一个文档和请求的文档是同一个域的,则unloadEventStart和unloadEventEnd分别代表浏览器unload前一个文档的开始和结束时间节点。否则两者都等于0;
unloadEventStart

DNS查询时间

= domainLookupEnd – domainLookupStart

TCP连接时间

=connectEnd – connectStart

HTTP请求时间

= responseEnd – responseStart

解析dom树耗时

= domComplete – domInteractive

白屏时间

= domloadng – fetchStart

domready时间

= domContentLoadedEventEnd – fetchStart

onload时间

= loadEventEnd – fetchStart

实现

PMS_A

/**
 * PMS header scripts
 *
 * 性能指标
 * wst: white screen timing
 * fst: first screen timing
 * drt: dom ready timing
 * prt: page ready timing
 * olt: on loaded timing
 * lpt: leave page timing
 *
 * dns: domain lookup timing
 * tct: tcp connect timing
 * rst: response start timing
 * ret: response end timing
 * dct: dom complete timing
 * let: load event timing
 * 
 * @author  Yang,junlong at 2017-05-04 17:18:50 build.
 * @version $Id$
 */

(function() {
    window.PMS = {
        _firsts: +new Date,
        _timing: {},
        _option: {},
        extend: function (destination) {
            if(!destination){
                return;
            }
            var args = Array.prototype.slice.call(arguments, 1);
            for (i = 0, l = args.length; i < l; i++) {
                var source = args[i];
                for(var property in source){
                    var value = source[property];
                    if (value !== undefined && destination[property] === undefined){
                        destination[property] = value;
                    }
                }
            }
            return destination;
        },
        // mark timing
        mark: function (markname, timestamp) {
            this._timing[markname] = timestamp || +new Date;
        },
        init: function (option) {
            this.extend(this._option, option);
        },
        fst: function () {
            this.mark('fst');
        },
        prt: function () {
            this.mark('prt');
        }
    }
})();

PMS_B

/**
 * Description
 * 
 * @author  Yang,junlong at 2017-05-04 17:45:39 build.
 * @version $Id$
 */

(function() {
    PMS.extend(PMS, {
        sended: false,
        metrics: {},
        each: function (object, callback, scope) {
            // array
            if (object.length === +object.length) {
                for (var i = 0, l = object.length; i < l; i++) {
                    if (callback.call(scope, object[i], i, object) === false) {
                        return;
                    }
                }
            } else {
                for (var key in object) {
                    if (object.hasOwnProperty(key)) {
                        if (callback.call(scope, object[key], key, object) === false) {
                            return;
                        }
                    }
                }
            }
        },
        setup: function () {
            // setup something
            var timing = {
                wst: 0, // white screen timing
                fst: 0, // first screen timing
                drt: 0, // dom ready timing
                prt: 0, // page ready timing
                olt: 0, // on load timing
                lpt: 0, // leave page timing
                dns: 0, // domain lookup timing
                tct: 0, // tcp connect timing
                rst: 0, // response start timing
                ret: 0, // response end timing
                dct: 0, // dom complete timing
                let: 0  // load event timing
            };

            this.extend(this._option, {
                sample: 0.1,
                logurl: 'https://stat.pay.xiaomi.com/event.gif',
                env: {}
            });

            this.each(this._option['env'], function(value, key) {
                this.metrics[key] = value;
            }, this);

            PMS._timing['prt'] = Math.max(PMS._timing['drt'], PMS._timing['prt'] || 0);

            this.measure(PMS._firsts, this._timing);

            // 设置屏幕信息
            // this.screen();

            this.measureNetworkTime();
        },

        measureNetworkTime: function () {
            if (window.performance && performance.timing) {
                var timing = performance.timing;
                var startTime = timing.navigationStart || timing.fetchStart;

                var _timing = {
                    dns: timing.domainLookupEnd - timing.domainLookupStart,
                    cnt: timing.connectEnd,
                    rst: timing.responseStart,
                    ret: timing.responseEnd,
                    dct: timing.domComplete,
                    let: timing.loadEventEnd,
                    wst: timing.domLoading
                };

                var wtt = PMS._firsts - startTime;
                wtt > 0 && (this.metrics['wtt'] =  wtt);

                this.measure(startTime, _timing);
            }
        },

        // measure timing
        measure: function (start, timing) {
            var _timing = 0;
            this.each(timing, function(value, key) {
                value = Math.max(value, this._timing[key] || 0);
                _timing = ('' + value).length < 8 ? value : value - start;
                this.metrics[key] = _timing;
            }, this);
        },


        ready: (function (callback) {
            var isReady = false;
            var readyList = [];
            // The ready event handler
            var DOMContentLoaded = function( event ) {
                // readyState === "complete" is good enough for us to call the dom ready in oldIE
                if ( document.addEventListener || event.type === 'load' || document.readyState === 'complete' ) {
                    detach();
                    ready();
                }
            };
            // Clean-up method for dom ready events
            var detach = function() {
                if ( document.addEventListener ) {
                    document.removeEventListener('DOMContentLoaded', DOMContentLoaded, false);
                    window.removeEventListener('load', DOMContentLoaded, false);
                } else {
                    document.detachEvent('onreadystatechange', DOMContentLoaded);
                    window.detachEvent('onload', DOMContentLoaded);
                }
            };
            // Handle when the DOM is ready
            function ready() {
                if(!isReady) {
                    isReady = true;
                    for (var i = 0, j = readyList.length; i < j; i++) {
                        readyList[i]();
                    }
                }
            }
            // Catch cases where $(document).ready() is called after the browser event has already occurred.
            // we once tried to use readyState "interactive" here, but it caused issues like the one
            // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
            if ( document.readyState === "complete" ) {
                // Handle it asynchronously to allow scripts the opportunity to delay ready
                setTimeout( ready );
            // Standards-based browsers support DOMContentLoaded
            }else if (document.addEventListener) {
                // Use the handy event callback
                document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);
                // A fallback to window.onload, that will always work
                window.addEventListener('load', ready, false);
            // If IE event model is used
            } else {
                // Ensure firing before onload, maybe late but safe also for iframes
                document.attachEvent('onreadystatechange', DOMContentLoaded);
                // A fallback to window.onload, that will always work
                window.attachEvent('onload', ready);
                // If IE and not a frame
                // continually check to see if the document is ready
                var top = false;
                try {
                    top = window.frameElement == null && document.documentElement;
                } catch(e) {}
                if (top && top.doScroll) {
                    (function() {
                        if (!isReady) {
                            try {
                                // Use the trick by Diego Perini
                                // http://javascript.nwbox.com/IEContentLoaded/
                                top.doScroll("left");
                            } catch (e) {
                                return setTimeout(arguments.callee, 1);
                            }
                            // and execute any waiting functions
                            ready();
                        }
                    })();
                }
            }
            return function(callback) {
                isReady ? callback() : readyList.push(callback);
                return isReady;
            }
            // end
        })(),

        // get first paint timing
        getFPT: function() {
            var fpt;
            if (window.performance && performance.timing && performance.timing.msFirstPaint) {
                fpt = performance.timing.msFirstPaint;
            } else {
                if (window.chrome && chrome.loadTimes) {
                    fpt = parseInt(chrome.loadTimes().firstPaintTime * 1000);
                }
            }
            return fpt;
        },

        // 获取屏幕尺寸
        screen: function () {
            if (window.screen) {
                this.metrics['screen'] =   window.screen.width + "*" + window.screen.height + "|" + window.screen.availWidth + "*" + window.screen.availHeight;
            }
        },

        send: function (delay) {
            var that = this;
            delay = delay || 0;
            setTimeout(function(){
                that._send.call(that);
            }, delay);
        },

        // send data
        _send: function() {
            if (this.sended) {
                return;
            }
            this.sended = true;
            // setup pms
            PMS.setup();

            var query = [];
            query.push('metric=' +  this._option.metric);
            PMS.each(this.metrics, function(item, key) {
                query.push('tags:'+key + '=' + item);
            });

            var logurl = this._option.logurl + '?' + query.join('&');
            this.stat(logurl);
        },
        stat: function (src) {
            var n = '__log_img_' + new Date * 1;

            // 将image对象赋给全局变量,防止被当做垃圾回收,造成请求失败。
            var img = window[n] = new Image();
            img.onload = img.onerror = function(){
                //垃圾回收
                window[n] = null; 
                delete window[n];
            };
            img.src = src;
            //垃圾回收
            img = null;
        }
    });


    // dom ready timing
    PMS.ready(function() {
        PMS.mark('drt');
    });
    if (document.attachEvent) {
        window.attachEvent("onload", function() {
            PMS.mark("olt")

            PMS.send();
        }, false);

        window.attachEvent("onbeforeunload", function() {
            // leave page timing
            PMS.mark("lpt");

            PMS.send();
        });
    } else {
        window.addEventListener("load", function() {
            PMS.mark("olt");

            PMS.send();
        });

        window.addEventListener("beforeunload", function() {
            // leave page timing
            PMS.mark("lpt");

            PMS.send();
        }, false);
    }
})();

参考

前端性能监控方案调研

这篇文章目前没有评论

Leave a Reply

(必填项)

(必填项)

(可选)