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: