nic jansma | SOASTA | nicj.net | @nicj
Nic Jansma
SOASTA
Real User Monitoring
readyState
changes and the onload
eventangular.js
), plus showing
the initial routeTraditional websites:
onload
event will fireSingle Page Apps:
angular.js
)onload
event fires here)onload
eventonload
at 1.225 secondsonload
fired 0.5 seconds too early!Single Page Apps:
onload
onload
Bad 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 fetchedSPA 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 willTransition
router.on("route")
XMLHttpRequest
(network activity) might indicate that the page's view
is being updatedWhen 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 completeXMLHttpRequest
s 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 FRAME
What'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.length
document.getElementsByTagName("*").length
window.onerror
ResourceTiming2
or XHRs
requestAnimationFrame
OK, 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: