Save 37% off PRO during our Black Friday Sale! »

ConFoo: Measuring SPA Performance

3de01a0c1a9c0e55efc6ecfa72b4eab3?s=47 Philip Tellis
February 25, 2016

ConFoo: Measuring SPA Performance

3de01a0c1a9c0e55efc6ecfa72b4eab3?s=128

Philip Tellis

February 25, 2016
Tweet

Transcript

  1. M E A S U R I N G S

    PA P E R F O R M A N C E ConFoo 2016 2016-02-25
  2. Philip Tellis @bluesmoon https://github.com/lognormal/boomerang http://www.soasta.com/mpulse/ Nic Jansma @nicj

  3. W H AT D O E S A PA G

    E L O O K L I K E O N T H E N E T W O R K ?
  4. performance.timing F U L L PA G E S A

    R E E A S Y T O M E A S U R E
  5. N AV I G AT I O N T I

    M I N G P E R F O R M A N C E T I M E L I N E https://www.w3.org/TR/navigation-timing-2/
  6. N AV I G AT I O N T I

    M I N G AVA I L A B I L I T Y • IE >= 9 • FF >= 7 • Chrome >= 6 • Safari >= 8 • Opera >= 15 • Latest Android, Blackberry, Opera Mobile, Chrome for Android, Firefox for Android, IE Mobile, and iOS http://caniuse.com/#search=navigation%20timing
  7. N AV I G AT I O N T I

    M I N G E X A M P L E var pageLoadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
  8. W H I C H PA G E C O

    M P O N E N T S A F F E C T P E R C E I V E D L AT E N C Y ?
  9. R E S O U R C E T I

    M I N G P E R F O R M A N C E T I M E L I N E https://www.w3.org/TR/resource-timing/
  10. R E S O U R C E T I

    M I N G AVA I L A B I L I T Y • IE >= 10 & Edge • Chrome • Firefox >= 37 • Opera >= 16 • Latest Opera Mobile, Chrome for Android, IE Mobile http://caniuse.com/#search=resource%20timing
  11. F U N W I T H R E S

    O U R C E T I M I N G • Generate a RUM waterfall • Or a performance heatmap 
 https://github.com/zeman/perfmap • Calculate a cache-hit-ratio per resource • Identify problem resources
  12. L I M I TAT I O N S O

    F R E S O U R C E T I M I N G • Does not report on Domain Lookup, Connect, Timeout or Security errors • Inconsistent reporting of 4xx/5xx errors • Chrome: No ResourceTiming information • Firefox: All timestamps are non-zero but duration is 0 • Internet Explorer: All timestamps are non-zero • Doesn’t tell you if a response is a 304 or 200
  13. C O R S • Cross-domain resources only tell you

    start & end time • Set the Timing-Allow-Origin: * response header on the resource to report all timings • Do this for resources served from your CDN or if you’re a 3rd party C R O S S - O R I G I N R E S O U R C E S H A R I N G
  14. B U T W H AT A B O U

    T S PA S ?
  15. W H AT D O E S A S PA

    L O O K L I K E O N T H E N E T W O R K ?
  16. C H A L L E N G E S

    • onload event doesn’t really matter • Soft Navigations are not really navigations • The browser does not tell us when soft navigations are complete
  17. M E A S U R I N G X

    H R S function instrumentXHR() { var proxy_XMLHttpRequest, orig_XMLHttpRequest = window.XMLHttpRequest, readyStateMap; if (!orig_XMLHttpRequest) { // Nothing to instrument return; } readyStateMap = [ "uninitialized", "open", "responseStart", "domInteractive", "responseEnd" ]; // We could also inherit from window.XMLHttpRequest, but for this implementation, // we'll use composition proxy_XMLHttpRequest = function() { var req, perf = { timing: {}, resource: {} }, orig_open, orig_send; req = new orig_XMLHttpRequest; orig_open = req.open; orig_send = req.send; req.open = function(method, url, async) { if (async) { req.addEventListener('readystatechange', function() { perf.timing[readyStateMap[req.readyState]] = new Date().getTime(); }, false); } req.addEventListener('load', function() { perf.timing["loadEventEnd"] = new Date().getTime(); perf.resource.status = req.status; }, false); req.addEventListener('timeout', function() { perf.timing["timeout"] = new Date().getTime(); }, false); req.addEventListener('error', function() { perf.timing["error"] = new Date().getTime(); }, false); req.addEventListener('abort', function() { perf.timing["abort"] = new Date().getTime(); }, false); perf.resource.name = url; perf.resource.method = method; // call the original open method return orig_open.apply(req, arguments); }; req.send = function() { perf.timing["requestStart"] = new Date().getTime(); // call the original send method return orig_send.apply(req, arguments); }; req.performance = perf; return req; }; window.XMLHttpRequest = proxy_XMLHttpRequest; }
  18. function instrumentXHR { var proxy_XMLHttpRequest orig_XMLHttpRequest readyStateMap if (!orig_XMLHttpRequest //

    Nothing to instrument return } readyStateMap // We could also inherit from window.XMLHttpRequest, but for this implementation, // we'll use composition proxy_XMLHttpRequest var req orig_open orig_send req req perf req perf perf req req req perf perf }; req perf }; req return }; window.XMLHttpRequest } M E A S U R I N G X H R S TL;DR: Proxy XMLHttpRequest Intercept open() & send() and capture events
  19. A C T U A L I M P L

    E M E N TAT I O N I S M O R E C O M P L I C AT E D , W H AT F O L L O W S I S S I M P L I F I E D , B U T R E P R E S E N TAT I V E WA R N I N G ! S I M P L I F I E D C O D E F O L L O W S Complete code at https://github.com/lognormal/boomerang/blob/master/plugins/auto_xhr.js
  20. L E T ’ S B R E A K

    I T D O W N — P R O X Y var orig_XMLHttpRequest = window.XMLHttpRequest; var proxy_XMLHttpRequest = function() { var req, resource = { timing: {}, initiator: "xhr" }, orig_open, orig_send; req = new orig_XMLHttpRequest(); orig_open = req.open; orig_send = req.send; // define new open // define new send req.resource = resource; return req; }; window.XMLHttpRequest = proxy_XMLHttpRequest;
  21. L E T ’ S B R E A K

    I T D O W N — S E N D req.send = function() { resource.timing.requestStart = BOOMR.now(); // call the original send method return orig_send.apply(req, arguments); }; In this method, we capture the start time of the XHR
  22. L E T ’ S B R E A K

    I T D O W N — O P E N var a = document.createElement("A") req.open = function(method, url, async) { a.href = url; // resolve relative URLs // Default value of async is true if (async === undefined) { async = true; } if (async) { // add listener on readystatechange } // add listeners on load, timeout, error & abort resource.url = a.href; resource.method = method; // call the original open method return orig_open.apply(req, arguments); };
  23. R E A D Y S TAT E C H

    A N G E F O R A S Y N C R E Q U E S T S • 0 == uninitialized • 1 == open • 2 == responseStart • 3 == domInteractive • 4 == responseEnd
  24. B U T W E C A N D O

    M O R E O N M O D E R N B R O W S E R S res = performance.getEntriesByName(a.href); /* res.redirectStart, res.redirectEnd, res.fetchStart, res.domainLookupStart, res.domainLookupEnd, res.connectStart, res.connectEnd, res.requestStart, res.responseStart, res.responseEnd */
  25. Thank You

  26. Thank You But wait, there’s more…

  27. I T ’ S 2 0 1 6 . T

    H E S I N S PA D O E S N O T S TA N D F O R S I M P L E !
  28. • The initial XHR fetches XML or JSON • This

    may reference Images, styles or JS Modules that need to be fetched • The combination of all these resources initiated by a user action is what we really want to measure
  29. E N T E R T H E 
 M

    U TAT I O N O B S E R V E R https://developer.mozilla.org/en/docs/Web/API/MutationObserver
  30. M U TAT I O N O B S E

    R V E R var o = {observer: null, timer: null}; function done(mutations) { if (o.timer) { clearTimeout(o.timer); o.timer = null; } if (callback) { callback.call(callback_ctx, mutations, callback_data); callback = null; } if (o.observer) { o.observer.disconnect(); o.observer = null; } } o.observer = new MutationObserver(done); if (timeout) { o.timer = setTimeout(done, o.timeout); } o.observer.observe(el, config);
  31. The rest of the code is complicated, you can see

    it at https://github.com/lognormal/boomerang/blob/master/plugins/auto_xhr.js but tl;dr…
  32. M U TAT I O N O B S E

    R V E R • A MutationObserver listens for DOM mutations • Start listening when an XHR returns; set a short timeout • Attach load and error event handlers to and set timeouts on any IMG, SCRIPT, LINK & IFRAME nodes. • Cached resources may not fire events at all, so also check ResourceTiming • The load event does not tell you when the element became visible
  33. M U TAT I O N O B S E

    R V E R S U P P O R T http://caniuse.com/#search=mutation%20observer • IE >= 11 & Edge • FF >= 14 • Chrome >= 18 • Safari >= 6 • Opera >= 15 • Latest Android, Blackberry, Opera Mobile, Chrome for Android, Firefox for Android, IE Mobile, and iOS
  34. I T G E T S M O R E

    C O M P L I C AT E D W H E N 2 + X H R S S TA R T C O N C U R R E N T LY
  35. We could have stopped here, but…

  36. S PA F R A M E W O R

    K S • Angular.js • Backbone.js • Ember.js • React.js • It’s Thursday, here’s a new framework.js
  37. A N G U L A R T I M

    E L I N E
  38. A N G U L A R . J S

    1. Listen for Angular.js routing events like $routeChangeStart 2. Call out to boomerang in your Angular app angular.module("app") .run(["$rootScope", function($rootScope) { var hadRouteChange = false; $rootScope.$on("$routeChangeStart", function() { hadRouteChange = true; }); function hookAngularBoomerang() { if (window.BOOMR && BOOMR.version) { if (BOOMR.plugins && BOOMR.plugins.Angular) { BOOMR.plugins.Angular.hook($rootScope, hadRouteChange); } return true; } } if (!hookAngularBoomerang()) { document.addEventListener("onBoomerangLoaded", hookAngularBoomerang); } }]);
  39. O T H E R F R A M E

    W O R K S A R E S I M I L A R
  40. W H AT A B O U T VA N

    I L L A J S ?
  41. L O O K AT T H E h i

    s t o r y A P I history.pushState, history.replaceState & the popstate event
  42. A N D C L I C K S …

  43. A N D C H E C K I F

    A N Y T H I N G “ I N T E R E S T I N G ” H A P P E N S N E X T !
  44. G E N E R I C A L LY

    M E A S U R I N G S O F T N AV I G AT I O N S 1. Listen for XHR, click event or history change 2. Start MutationObserver and wait about 500ms for the DOM to change 3. If it did change, monitor all assets added to the page 4. In case of JavaScript additions, we might need to extend the MutationObserver 5. For images, check image.naturalWidth to tell if the image is already in cache or not. 6. Use setImmediate() to let the browser finish DOM layout before measuring end time.
  45. FetchObserver to monitor all downloads A N D I N

    T H E F U T U R E https://fetch.spec.whatwg.org/
  46. B U T I T ’ S V E RY

    C L O S E T H I S I S N ’ T P E R F E C T
  47. L O O K F O R M E M

    O RY L E A K S , F O R E X A M P L E W E S H O U L D A L S O M E A S U R E PA G E L I F E C Y C L E
  48. M E A S U R I N G PA

    G E L I F E C Y C L E • Memory usage: window.performance.memory (Chrome) • DOM Length (bytes): documentElement.innerHTML.length • DOM Nodes: document.getElementsByTagName(“*").length • JavaScript Errors: window.onerror • Bytes Fetched: ResourceTiming2 or XHRs • Frame rate: requestAnimationFrame
  49. Thank You http://soasta.io/SPAperfbook

  50. Philip Tellis @bluesmoon https://github.com/lognormal/boomerang http://www.soasta.com/mpulse/ Nic Jansma @nicj

  51. I M A G E C R E D I

    T S • Sandy’s Rain by woodleywonderworks
 https://www.flickr.com/photos/wwworks/8139513400/ • Oso Spa by Cristian Ruz
 https://www.flickr.com/photos/cruz_fr/4219863025/ • Pup! Watch Out! by John&Fish
 https://www.flickr.com/photos/johnfish/3592090719/