Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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 ?

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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/

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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;

Slide 8

Slide 8 text

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 ?

Slide 9

Slide 9 text

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/

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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 ?

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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;

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Thank You

Slide 26

Slide 26 text

Thank You But wait, there’s more…

Slide 27

Slide 27 text

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 !

Slide 28

Slide 28 text

• 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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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…

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

We could have stopped here, but…

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

A N D C L I C K S …

Slide 43

Slide 43 text

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 !

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Thank You http://soasta.io/SPAperfbook

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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/