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

JS@PayPal 2015 - Insanely fast rendering w/ Service Workers and Early Flushing

Mark Stuart
December 14, 2015

JS@PayPal 2015 - Insanely fast rendering w/ Service Workers and Early Flushing

Insanely fast rendering w/ Service Workers and Early Flushing

Mark Stuart

December 14, 2015
Tweet

More Decks by Mark Stuart

Other Decks in Programming

Transcript

  1. Insanely Fast Rendering!
    w/ Early Flushing and Service Workers
    JS@PayPal Winter 2015 - 12/14

    View full-size slide

  2. go/serviceworkers

    View full-size slide

  3. Hi, I’m Mark!
    @mark_stuart
    @marstuart

    View full-size slide

  4. Let's talk about pushing
    pixels to the browser
    as fast as possible.

    View full-size slide

  5. Early Flushing

    View full-size slide

  6. By default,
    HTTP responses are buffered.

    View full-size slide

  7. Browsers can’t render
    anything until that 1st HTML
    response is received. :(

    View full-size slide

  8. OK, so what can we do about it?

    View full-size slide

  9. What if we chunked up that HTML
    response into multiple parts?

    View full-size slide

  10. Browsers can start downloading and
    parsing critical JS and CSS
    before the HTML response is finished.

    View full-size slide

  11. No Early Flush
    DOMLoaded- 3.5s
    Load - 3.8s
    First paint - 3.4s
    Early Flush
    DOMLoaded- 3.4s
    Load - 3.5s
    First paint - 695ms
    Saved 2.8s!

    View full-size slide

  12. Good news!
    You can use this too!

    View full-size slide

  13. Add a few headers…
    Flush out your scripts, styles, etc.
    Call your services, resolve locale, whatever else, then flush again.
    !
    !
    !
    1 res.setHeader('Transfer-Encoding', 'chunked');
    2
    3 // For Slingshot...
    4 res.setHeader('X-SLR-EARLY-FLUSH', '1');
    5
    6 // For Akamai...
    7 res.setHeader('X-Akamai-Stream', 'True');
    res.write(renderTemplate('first-flush.html', context));
    res.write(renderTemplate('second-flush.html', context));
    res.end();

    View full-size slide

  14. Try it out! Just a few headers.
    Took a lot of work to pull this off.
    Ready for production after moratorium.

    View full-size slide

  15. With early flushing, we render at
    695ms. But, that’s still not fast enough.
    I’d like to introduce you to
    Service Workers!

    View full-size slide

  16. Flush early!

    View full-size slide

  17. Service Workers

    View full-size slide

  18. Apps that use Service Workers today
    - Facebook - Push notifications
    - Pinterest - Push notifications
    - Medium - Caching
    - Google’s Inbox app - Caching & Push notifications

    View full-size slide

  19. Browser Support
    - Chrome 40+
    - Firefox 44+
    - Opera 24+
    - Android 46
    - Safari and IE to come later.

    View full-size slide

  20. What’s a Service Worker?

    View full-size slide

  21. - Similar to a Web Worker
    - Can’t directly access the DOM
    - Communicates w/ parent by postMessage
    - Can import scripts via importScripts
    - Promise-based APIs
    - Easy debugging

    View full-size slide

  22. Features
    - Able to intercept and handle network requests
    - Complete control of the browser’s cache
    - Push notifications
    - Background sync
    - Geofencing

    View full-size slide

  23. Most exciting feature?
    Being able to intercept and handle network requests!

    View full-size slide

  24. A few interesting use cases

    View full-size slide

  25. Serving assets from cache
    self.addEventListener('fetch', function (event) {
    event.respondWith(
    caches.open('my-cool-cache').then(function (cache) {
    return cache.match(event.request).then(function (response) {
    if (response) {
    return response;
    }
    return fetch(event.request.clone()).then(function (response) {
    if (response.status < 400) {
    cache.put(event.request, response.clone());
    }
    return response;
    });
    });
    });
    );
    });
    !
    !
    !

    View full-size slide

  26. Enforce SLA on 3rd party scripts !
    !
    !
    self.addEventListener('fetch', function (event) {
    // If it's a 3rd party script, enforce a 2s SLA, then timeout
    if (thirdPartyScripts.includes(event.request.url)) {
    return event.respondWith(Promise.race([
    timeout(2000),
    fetch(event.request.url)
    ]));
    }
    event.respondWith(fetch(event.request.url));
    });
    // FPTI and Fraudnet scripts
    var thirdPartyScripts = [
    'https://www.paypalobjects.com/pa/js/pa.js',
    'https://www.paypalobjects.com/webstatic/r/fb/fb-all-prod.pp.min.js'
    ];
    function timeout(delay) {
    return new Promise(function (resolve) {
    setTimeout(function () {
    resolve(new Response('', {
    status: 408,
    statusText: 'Request timed out'
    }));
    }, delay);
    });
    }

    View full-size slide

  27. Enforce SLA on 3rd party scripts !
    !
    !
    // FPTI and Fraudnet scripts
    var thirdPartyScripts = [
    'https://www.paypalobjects.com/pa/js/pa.js',
    'https://www.paypalobjects.com/webstatic/r/fb/fb-all-prod.pp.min.js'
    ];
    function timeout(delay) {
    return new Promise(function (resolve) {
    setTimeout(function () {
    resolve(new Response('', {
    status: 408,
    statusText: 'Request timed out'
    }));
    }, delay);
    });
    }
    self.addEventListener('fetch', function (event) {
    // If it's a 3rd party script, enforce a 2s SLA, then timeout
    if (thirdPartyScripts.includes(event.request.url)) {
    return event.respondWith(Promise.race([
    timeout(2000),
    fetch(event.request.url)
    ]));
    }
    event.respondWith(fetch(event.request.url));
    });

    View full-size slide

  28. Supporting WebP images !
    !
    !

    self.addEventListener('fetch', function (event) {
    var request = event.request;
    var url = request.url;
    var isImage = /\.jpg$|.png$/.test(request.url);
    var supportsWebp = request.headers.get('accept').includes('webp');
    if (isImage && supportsWebp) {
    url = request.url.substr(0, request.url.lastIndexOf('.')) + '.webp';
    }
    return event.respondWith(fetch(url));
    });

    View full-size slide

  29. Responsive images !
    !
    !

    Use images like you normally would…
    1. Intercept that image request
    2. If “retina” support (or device-pixel-ratio >= 2), re-write the URL
    as “[email protected]
    3. Request that from the network

    View full-size slide

  30. Defer requests while offline !
    !
    !
    If a network request fails, or user goes offline…
    You can cache requests and
    re-play them when the user goes back online.

    View full-size slide

  31. Mock Server? lol !
    !
    !
    Might be a really crazy idea, but it’s possible!
    Need to run in mocks mode?
    Use a Service Worker!
    Intercept API responses and send back mock responses
    No need to run a server

    View full-size slide

  32. Service Workers are great for PayPal use cases
    - Offline support - Send/Request money (P2P)
    - Rendering shells - Dashboard apps like Hawk and 8Ball all have
    this “shell” on the ourside that could be instantly rendered while
    the remaining bits are loaded afterwards.

    View full-size slide

  33. Application Shell Architecture
    - Coined by Addy Osmani, from the Chrome team
    - With Service Workers, you can instantly load your page on
    repeat visits.
    - Render the Shell or Chrome of your page immediately.
    - Lazy load in the rest of your page

    View full-size slide

  34. Our Caching Strategy (Starting Q1)
    - Always read static/framework assets from cache (jQuery,
    Angular, etc.)
    - Always immediately render "shell" from cache
    - Fetch from network, by default. If success, cache response.
    - If any network issues or fallbacks, read from cache.
    - Add a 2 second SLA on 3rd party scripts

    View full-size slide

  35. Questions?
    #serviceworkers on Node.js Slack
    (nodejs.slack.com)

    View full-size slide