Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ConFoo: Measuring SPA Performance

Philip Tellis
February 25, 2016

ConFoo: Measuring SPA Performance

Philip Tellis

February 25, 2016
Tweet

More Decks by Philip Tellis

Other Decks in Technology

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. 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 ?
  3. 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
  4. 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/
  5. 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
  6. 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;
  7. 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 ?
  8. 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/
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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 ?
  14. 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
  15. 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; }
  16. 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
  17. 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
  18. 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;
  19. 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
  20. 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); };
  21. 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
  22. 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 */
  23. 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 !
  24. • 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
  25. 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
  26. 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);
  27. 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…
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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); } }]);
  33. 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
  34. W H AT A B O U T VA N

    I L L A J S ?
  35. 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
  36. 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 !
  37. 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.
  38. FetchObserver to monitor all downloads A N D I N

    T H E F U T U R E https://fetch.spec.whatwg.org/
  39. 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
  40. 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
  41. 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
  42. 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/