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

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

@jakob_anderson - Twitter

@jakob - FHD Slack

jakobanderson.com

Fast and Visible

By Jakob Anderson

Fast and Visible

How to measure page performance without hurting it

  • 15,552