nic jansma | SOASTA | nicj.net | @nicj
                        Nic Jansma
SOASTA
                    
                    Real User Monitoring
readyState changes and the onload event
                    
                    
                    
                    
                    angular.js), plus showing
                                the initial routeTraditional websites:
onload event will fire
                    Single Page Apps:
angular.js)onload event fires here)onload event
                    
                        onload at 1.225 secondsonload fired 0.5 seconds too early!
                    Single Page Apps:
onloadonloadBad for traditional RUM tools:
readyState, onload) and metrics (NavigationTiming) are all geared toward a single load eventonload only onceonload event helps us know when all static content was fetchedonload event again,
                                so we don't know when its content was fetched
                    SPA soft navigations may fetch:
SPA frameworks often fire events around navigations. AngularJS events:
$routeChangeStart: When a new route is being navigated to$viewContentLoaded: Emitted every time the ngView content is reloadedBut neither of these events have any knowledge of the work they trigger, fetching new IMGs, CSS, JavaScript, etc!
$routeChangeStart$viewContentLoaded<img>, <javascript>, etc.
                    We need to figure out at what point the navigation started (the start event), through when we consider the navigation complete (the end event).
For hard navigations:
navigationStart if available,
                                to know when the browser navigation beganChallenge #2: Soft navigations are not real navigations
The window.history object can tell
                            us when the URL is changing:
pushState or replaceState are being called, the app is possibly
                                updating its viewwindow.popstate event is fired, and the app
                                will possibly update the viewSPA frameworks fire routing events when the view is changing:
$rootScope.$on("$routeChangeStart")beforeModel or willTransitionrouter.on("route")XMLHttpRequest (network activity) might indicate that the page's view
                                is being updated
                    When do we consider the SPA navigation complete?
There are many definitions of complete:
Traditional RUM measures up to the onload event:
Which resources could affect visual completion of the page?
For hard navigations, the onload event no longer matters (Challenge #1)
onload event only measures up to when all static resources were fetchedFor soft navigations, the browser won’t tell you when all resources have been downloaded (Challenge #3)
onload only fires once on a pageLet's make our own SPA onload event:
onload event, let's wait for all network activity to completeXMLHttpRequests play an important role in SPA frameworks
XMLHttpRequest object can be proxied.open() and .send() methods to know when
                                an XHR is startingSimplified code ahead!
Full code at github.com/lognormal/boomerang/blob/master/plugins/auto_xhr.js
var orig_XHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
    var req = new orig_XHR();
    orig_open = req.open;
    orig_send = req.send;
    req.open = function(method, url, async) {
        // save URL details, listen for state changes
        req.addEventListener("load", function() { ... });
        req.addEventListener("timeout", function() { ... });
        req.addEventListener("error", function() { ... });
        req.addEventListener("abort", function() { ... });
        orig_open.apply(req, arguments);
    };
    req.send = function() {
        // save start time
        orig_send.apply(req, arguments);
    }
}
                    By proxying the XHR code, you can:
Downsides:
XHR is the main way to fetch resources via JavaScript
Image object as that only works if you create a new Image()
                                in JavaScripthttp://developer.mozilla.org/en-US/docs/Web/API/MutationObserver:
MutationObserver provides developers a way to react to changes in a DOM
Usage:
observe() for specific eventsSimplified code ahead!
Full code at github.com/lognormal/boomerang/blob/master/plugins/auto_xhr.js
var observer = new MutationObserver(observeCallback);
observer.observe(document, {
    childList: true,
    attributes: true,
    subtree: true,
    attributeFilter: ["src", "href"]
});
                    
function observeCallback(mutations) {
    var interesting = false;
    if (mutations && mutations.length) {
        mutations.forEach(function(mutation) {
            if (mutation.type === "attributes") {
                interesting |= isInteresting(mutation.target);
            } else if (mutation.type === "childList") {
                for (var i = 0; i < mutation.addedNodes.length; i++) {
                    interesting |= isInteresting(mutation.addedNodes[i]);
                }
            }
        });
    }
    if (!interesting) {
        // complete the event after N milliseconds if nothing else happens
    }
});
                    Simplified workflow:
MutationObserver to listen for DOM mutationsload and error event handlers and set timeouts
                                on any IMG, SCRIPT, LINK or FRAMEWhat's interesting to observe?
IMG elements that haven't already been fetched (naturalWidth==0),
                                have external URLs (e.g. not data-uri:) and that we haven't seen before.SCRIPT elements that have a src setIFRAMEs elements that don't have javascript: or about: protocolsLINK elements that have a href setDownsides:
                    Polyfills (with performance implications):
It's not just about navigations
What about components, widgets and ads?
How do you measure visual completion?
Challenges:
IMG has been fetched, that's not when it's displayed to the visitor (it has to decode, etc.)Use setTimeout(..., 0) or setImmediate to get a callback after the browser has finished
                            parsing some DOM updates
var xhr = new XMLHttpRequest();
xhr.open("GET", "/fetchstuff");
xhr.addEventListener("load", function() {
    $(document.body).html(xhr.responseText);
    setTimeout(function() {
        var endTime = Date.now();
        var duration = endTime - startTime;
    }, 0);
});
var startTime = Date.now();
xhr.send();
                    This isn't perfect:
What happens over time?
How well does your app behave?
It's not just about measuring interactions or how long components take to load
Tracking metrics over time can highlight performance, reliability and resource issues
You could measure:
window.performance.memory (Chrome)document.documentElement.innerHTML.lengthdocument.getElementsByTagName("*").lengthwindow.onerrorResourceTiming2 or XHRsrequestAnimationFrameOK, that sounded like a lot of work-arounds to measure Single Page Apps.
Yep.
Why can't the browser just tell give us performance data for SPAs in a better, more performant way?
Instead of instrumenting XMLHttpRequest and using MutationObserver to find new
                            elements that will fetch: