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

    View full-size slide

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

    View full-size slide

  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 ?

    View full-size slide

  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

    View full-size slide

  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/

    View full-size slide

  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

    View full-size slide

  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;

    View full-size slide

  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 ?

    View full-size slide

  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/

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  14. B U T W H AT
    A B O U T S PA S ?

    View full-size slide

  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 ?

    View full-size slide

  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

    View full-size slide

  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;
    }

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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;

    View full-size slide

  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

    View full-size slide

  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);
    };

    View full-size slide

  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

    View full-size slide

  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
    */

    View full-size slide

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

    View full-size slide

  26. 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 !

    View full-size slide

  27. • 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

    View full-size slide

  28. 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

    View full-size slide

  29. 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);

    View full-size slide

  30. 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…

    View full-size slide

  31. 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

    View full-size slide

  32. 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

    View full-size slide

  33. 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

    View full-size slide

  34. We could have
    stopped here, but…

    View full-size slide

  35. 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

    View full-size slide

  36. A N G U L A R T I M E L I N E

    View full-size slide

  37. 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);
    }
    }]);

    View full-size slide

  38. 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

    View full-size slide

  39. W H AT A B O U T VA N I L L A
    J S ?

    View full-size slide

  40. 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

    View full-size slide

  41. A N D C L I C K S …

    View full-size slide

  42. 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 !

    View full-size slide

  43. 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.

    View full-size slide

  44. FetchObserver to monitor all
    downloads
    A N D I N T H E F U T U R E
    https://fetch.spec.whatwg.org/

    View full-size slide

  45. 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

    View full-size slide

  46. 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

    View full-size slide

  47. 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

    View full-size slide

  48. Thank You
    http://soasta.io/SPAperfbook

    View full-size slide

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

    View full-size slide

  50. 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/

    View full-size slide