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 Slide

  2. go/serviceworkers

    View Slide

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

    View Slide

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

    View Slide

  5. Early Flushing

    View Slide

  6. By default,
    HTTP responses are buffered.

    View Slide

  7. View Slide

  8. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. View Slide

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

    View Slide

  14. 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 Slide

  15. Good news!
    You can use this too!

    View Slide

  16. 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 Slide

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

    View Slide

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

    View Slide

  19. Flush early!

    View Slide

  20. Service Workers

    View Slide

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

    View Slide

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

    View Slide

  23. What’s a Service Worker?

    View Slide

  24. - 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 Slide

  25. View Slide

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

    View Slide

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

    View Slide

  28. A few interesting use cases

    View Slide

  29. 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 Slide

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

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

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

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

  34. 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 Slide

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

  36. 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 Slide

  37. 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 Slide

  38. View Slide

  39. 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 Slide

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

    View Slide