Fast and Visible
How to measure page performance
without hurting it
Jakob Anderson
FamilySearch
Jakob Anderson
- FamilySearch
- Frontier Stack Maintainer
- User Champion
- Obsessed with Performance
- Robot fighting for all humans
Concepts
Goals of a Fast Site
- Fastest time to conversion
- Load faster than user can think*
- Lowest possible bandwidth & CPU cost
- Never get in user's way*
*"Doherty Threshold", IBM, 1982
http://daverupert.com/2015/06/doherty-threshold/
Doherty
Threshold
Productivity spikes
< 400ms
per response
Some important ways to do this
- Reduce payload
- Reduce # of calls during render
- Reduce CPU congestion during render
- Remove "site taxes": Everything that doesn't give the user immediate value
- Lazy-load low-priority content
Some perf goals
- 3s on mobile 3G, mobile CPU
- Lazy-load everything not in initial view
- "PRPL 50", from google
- < 200K payload
- Baseline template is around ~90K
- ~110K left for app + analytics
"Synth"
&
"RUM"
You need both,
neither replaces the other
Synth
Synthetic Performance Testing
- Practically no performance impact to user
- Great for measuring app perf changes
RUM
Real User Monitoring
- Passive
- Realistic
- International
- Live
"Site Tax" Dangers
- Render-blocking
- Head-of-Line network blocking
- Large payload
- Contending for CPU
- Not optimized for global users
- High bandwidth cost
- Reduce battery life if too chatty
- Single Point of Failure (SPOF)
RUM of Today
Options
- App Dynamics
- New Relic
- DynaTrace
- Boomerang.js + NodeJS + Splunk
Drawbacks
- Client scripts are kinda big
- Still coded for 10yo browsers
- One still requires flash on dashboard
- Coded for general use cases, diluted
RUM
OF
TOMORROW !!!
- User-focused
- Minimal browser work
- window.performance
- less browser edge cases
- Very light network use
RUM of tomorrow
What would it look like?
Tiny client lib
- < 1KB
- Loaded async
<script src="/photon-beacon/photon-beacon.min.js" async></script>
https://github.com/spacerockzero/photon-beacon
Collects Performance API Timings
gather () {
let data = {}
let perf = performance
// gather marks
data.marks = perf.getEntriesByType('mark')
// gather measures
data.measures = perf.getEntriesByType('measure')
// gather timings
data.timings = perf.timing
// first paint
if (window.chrome) {
data.firstPaint = window.chrome.loadTimes().firstPaintTime
}
// gather resources
data.resources = perf.getEntriesByType('resource')
return data
}
Pass arbitrary data, too
PHOTON.addData('foo', {'bar': false});
Collect late,
Send without blocking
<script>
window.PHOTON_CONFIG = {
URL: '/beacon'
}
// gather and send on unload, as user leaves page
window.addEventListener('unload', () => {
console.log('unload fired!');
PHOTON.getData(); // gather late, to gather without impacting user
PHOTON.send(); // sends beacon, if supported
});
</script>
send () {
let data = PHOTON.data
if (navigator.sendBeacon && data) {
let blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
})
navigator.sendBeacon(PHOTON.config.URL, blob)
}
}
Make beacon payload as small as possible (1/3)
// raw user-timing mark data
[
{
"name": "test1",
"entryType": "mark",
"startTime": 232.525,
"duration": 0
},
{
"name": "test2",
"entryType": "mark",
"startTime": 733.5550000000001,
"duration": 0
}
]
// raw measure data
[
{
"name": "measure1",
"entryType": "measure",
"startTime": 232.525,
"duration": 501.0300000000001
}
]
// Compressed user timing marks and measures
{
"test1": "6h",
"test2": "ke",
"measure1": "6h_dx"
}
npm: usertiming-compression
Make beacon payload as small as possible (2/3)
[
{
"name": "http://localhost:3000/test/usertiming-compression.js",
"entryType": "resource",
"startTime": 58.29,
"duration": 177.86000000000004,
"initiatorType": "script",
"workerStart": 0,
"redirectStart": 0,
"redirectEnd": 0,
"fetchStart": 58.29,
"domainLookupStart": 58.29,
"domainLookupEnd": 58.29,
"connectStart": 58.29,
"connectEnd": 58.29,
"secureConnectionStart": 0,
"requestStart": 63.27500000000001,
"responseStart": 67.65,
"responseEnd": 236.15000000000003,
"transferSize": 2021,
"encodedBodySize": 1659,
"decodedBodySize": 4038
},
{
"name": "http://localhost:3000/test/resourcetiming-compression.js",
"entryType": "resource",
"startTime": 58.415,
"duration": 178.08,
"initiatorType": "script",
"workerStart": 0,
"redirectStart": 0,
"redirectEnd": 0,
"fetchStart": 58.415,
"domainLookupStart": 65.74500000000002,
"domainLookupEnd": 65.76,
"connectStart": 65.76,
"connectEnd": 66.19,
"secureConnectionStart": 0,
"requestStart": 66.27000000000001,
"responseStart": 68.855,
"responseEnd": 236.495,
"transferSize": 3429,
"encodedBodySize": 3066,
"decodedBodySize": 9208
},
{
"name": "http://localhost:3000/photon-beacon/photon-beacon.min.js",
"entryType": "resource",
"startTime": 58.54500000000001,
"duration": 178.195,
"initiatorType": "script",
"workerStart": 0,
"redirectStart": 0,
"redirectEnd": 0,
"fetchStart": 58.54500000000001,
"domainLookupStart": 58.54500000000001,
"domainLookupEnd": 58.54500000000001,
"connectStart": 58.54500000000001,
"connectEnd": 58.54500000000001,
"secureConnectionStart": 0,
"requestStart": 72.68,
"responseStart": 73.68,
"responseEnd": 236.74,
"transferSize": 871,
"encodedBodySize": 557,
"decodedBodySize": 557
}
]
// Compressed resource timing
{
"restiming": {
"http://0003:tsohlacol/": {
"test/": {
"|": "6,h",
"usertiming-compression.js": "31m,4y,a,5*11a3,a2,1u3",
"resourcetiming-compression.js": "31m,4y,b,8,8,,8,8,8*12d6,a3,4qm"
},
"photon-beacon/photon-beacon.min.js": "31n,4y,f,e*1fh,8q"
}
},
"servertiming": []
}
npm: resourcetiming-compression
Make beacon payload as small as possible (3/3)
{
marks: [{
name: 'test1',
entryType: 'mark',
startTime: 64.57000000000001,
duration: 0
}, {
name: 'test2',
entryType: 'mark',
startTime: 565.605,
duration: 0
}],
measures: [{
name: 'measure1',
entryType: 'measure',
startTime: 64.57000000000001,
duration: 501.035
}],
timings: {
navigationStart: 1507589843812,
unloadEventStart: 1507589843859,
unloadEventEnd: 1507589843862,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 1507589843812,
domainLookupStart: 1507589843855,
domainLookupEnd: 1507589843855,
connectStart: 1507589843855,
connectEnd: 1507589843855,
secureConnectionStart: 1507589843812,
requestStart: 1507589843855,
responseStart: 1507589843856,
responseEnd: 1507589843856,
domLoading: 1507589843859,
domInteractive: 1507589843877,
domContentLoadedEventStart: 1507589843877,
domContentLoadedEventEnd: 1507589843877,
domComplete: 1507589843895,
loadEventStart: 1507589843895,
loadEventEnd: 1507589843896,
timeToNonBlankPaint: 1507589843883
},
resources: [{
name: 'http://localhost:3000/photon-beacon/photon-beacon.min.js',
entryType: 'resource',
startTime: 61.880751,
duration: 15.633134000000005,
initiatorType: 'script',
nextHopProtocol: 'http/1.1',
redirectStart: 0,
redirectEnd: 0,
fetchStart: 61.880751,
domainLookupStart: 71.244542,
domainLookupEnd: 71.28144999999999,
connectStart: 71.351761,
connectEnd: 71.650852,
secureConnectionStart: 0,
requestStart: 71.728572,
responseStart: 77.453355,
responseEnd: 77.513885,
transferSize: 871,
encodedBodySize: 557,
decodedBodySize: 557
}],
pageDurations: {
loadTime: 84,
DOMContentLoaded: 65,
timeToFirstByte: 44,
firstPaint: undefined,
frontEndTime: 40,
serverConnectionTime: 43,
pageRenderTime: 19
},
resourceDurations: [{
name: 'http://localhost:3000/photon-beacon/photon-beacon.min.js',
redirectTime: undefined,
dnsTime: 0.03690799999999683,
tcpHandshakeTime: 0.2990910000000042,
secureConnectionTime: undefined,
responseTime: 0.06052999999999997,
fetchUntilResponseEnd: 15.633134000000005,
requestStartUntilResponseEnd: 5.785313000000002,
startUntilResponseEnd: 15.633134000000005
}]
}
5d 00 00 01 00 b2 09 00 00 00 00 00 00 00 3d 88
09 a6 54 63 65 43 96 af c6 6e e3 8a cf 01 c4 5d
32 7d d7 2f 33 03 75 09 0c 49 44 90 d0 15 3f a6
75 07 51 03 1d 75 28 f3 b1 00 19 6f e5 2c ac 61
ae fa 0b 30 27 9e 0e ea 1c 68 0c f7 c9 bf c0 af
99 2b d2 b5 53 57 6b 4f 56 06 33 5e 9a 89 e2 7e
c7 bd db d8 c4 43 97 54 7b cf 29 1c 33 6c 9e d4
c4 de 36 42 5a 72 ef 36 da 4d a9 9e 9c 2c 1a 95
ed 86 c3 35 fd 54 55 c1 98 86 2b 60 b8 fe 0d 66
01 75 6b 50 02 7c 48 f4 03 26 6a 99 02 81 91 3f
c5 63 8e ed 5e 7b fa 52 cb eb a9 cb a7 a0 fb 92
be c5 7e 0e f3 86 be 10 99 4f 1f 81 58 59 ca 68
b5 1c 05 2c de 71 37 3a db a6 05 da a7 64 f2 7e
77 38 dc 90 3e 02 6c 8d cd 39 8f ef 83 90 4a 6e
9b c4 51 3c 71 40 0a 6d 04 6a e5 b7 e4 d3 01 47
ad 24 53 6e 49 9b 9c 46 2f 4e 18 90 6f 10 10 18
c7 8f 8b c6 70 dc a9 f4 10 4e bf 0b 0f cf 51 d5
d3 6d 26 ae f7 3f 1e 05 4d b6 6d c1 f6 dc fd fd
53 f9 4e 9a 49 3e 48 f5 ed 7f 12 86 54 87 98 49
91 cc bb 0c da 2a 9c eb fa 3c d0 71 7f a6 29 fa
ba f1 b1 41 bd 92 7f a9 0c ce 20 3f 22 d3 f6 7b
d2 02 40 82 43 0d bb 37 74 70 48 e1 15 a3 6d 47
4d 04 47 54 4a c8 1d f5 12 44 1c c3 9b c6 7d e7
74 30 70 a4 d2 f0 87 5c b2 6e cd f9 eb 25 7f 19
18 b3 3e a5 d9 1d 9c 9e e7 74 e1 1c 38 58 60 17
e6 1d 91 b2 58 3a 2c c3 29 22 46 2b 8d 6c 0c 72
38 a4 13 b5 96 9a c1 d1 1e d6 c0 1f 68 c5 c3 39
fc 9a 03 e8 e1 53 1f 08 f7 85 86 3f 29 c4 b3 1d
f6 ac 99 a5 4f a8 ce 50 b6 f3 77 7a 39 29 65 63
b1 ec 8a b3 ab 2e 1c c5 19 ff 68 c9 b2 d6 6e 9f
c3 10 5a 06 b6 6b c8 ea 52 6a b6 0a 05 10 4e 62
c5 85 45 34 3d f3 76 0f 41 86 04 ec 44 7f 1d 13
79 eb ae 8b 5c 2c 80 4d f4 25 a0 3f bf 23 75 46
09 96 bc 0a 79 bc ce 08 96 7c 71 6b 8a c6 9f 7b
2f fe 96 10 e6 33 6b 4e 05 55 29 e9 01 2d cc 6d
9e 95 49 25 a8 c8 c9 8f 33 bf e3 f9 99 c1 51 6a
a2 4c f0 b9 1f a4 05 f4 12 f6 45 1f 41 b4 8c 0a
7a a0 f2 7c 13 db 79 0c 18 73 ae 79 e2 a4 7e a1
ae db 0d a7 de a9 b7 9e a9 8c 74 a4 2e 6e ac 52
ba 34 8f e6 f8 48 20 e7 23 98 aa b7 37 a4 b6 21
2e bf ba 15 58 6c 8f 2c e0 6b af e4 f3 c7 3c 48
18 bc b3 55 64 89 1f 20 2f a5 86 b4 c5 4f 5e 61
47 f8 1a 11 09 21 a0 ad 22 ab b6 af 6e 2d 43 18
50 8b 8d e6 c8 50 7b 64 0f 72 bc fc fc 75 e8 f4
27 07 7a 92 8e 34 de 81 42 d0 28 18 b7 da 17 a9
50 df 2f f5 8d f9 9d 78 62 fb 40 66 cb 4d 8d b0
15 fb ee 9a cd 92 a8 93 a6 d2 9a 06 1b ff 9a 9f
c7 b7
npm: lz-string
4964 bytes
754 bytes
Fast and lightweight collector (1/3)
// NodeJS, Express
const express = require('express')
const router = express.Router()
const beaconLogger = require('../lib/beacon-logger')
/* POST photon-beacon */
router.post('/beacon', beaconLogger)
https://github.com/spacerockzero/photon-server
Fast and lightweight collector (2/3)
const doDecompress = require('./decompress')
const doTimingCalculations = require('./timing-calculations')
module.exports = (req, res, next) => {
// Parse beacon data
let data = req.body
// if fields are compressed, decompress them
data = doDecompress(data)
// do all calculations
data = doTimingCalculations(data)
// Write log
console.log('PHOTON_DATA:', data)
res.sendStatus(204)
}
https://github.com/spacerockzero/photon-server
Fast and lightweight collector (2/3)
...
data.pageDurations = {
loadTime: getDuration(data.navigationStart, data.loadEventEnd),
DOMContentLoaded: getDuration(data.navigationStart, data.domContentLoadedEventEnd),
timeToFirstByte: getDuration(data.navigationStart, data.responseStart),
firstPaint: getDuration(data.navigationStart, data.firstPaint),
frontEndTime: getDuration(data.responseStart, data.loadEventEnd),
serverConnectionTime: getDuration(data.navigationStart, data.requestStart),
pageRenderTime: getDuration(data.domContentLoadedEventEnd, data.loadEventEnd)
}
...
https://github.com/spacerockzero/photon-server
Big-data analysis (1/3)
frontEndTime=40
Some data we logged
Big-data analysis (2/3)
index=production sourcetype=heroku*
fs_host="fs-client-logger" app=home
| timechart span=1d p50(frontEndTime) as FrontEnd
Splunk Query to analyze trend
Big-data analysis (3/3)
Timechart of Trend
Client
Collector
Analysis
When can I use the ...
RUM
OF
TOMORROW?
83%
76%
94%
~80% Users
Supported Now
Fight for the Users
Fast and Visible
By Jakob Anderson
Fast and Visible
How to measure page performance without hurting it
- 18,200