Service Workers: Resources, Tips and Solutions

This post is over six (6) months old. Some things on this page my be out of date or no longer applicable.


I was, and still am, super excited about ServiceWorkers. More for the (possible) performance increase and offline fallback/offline aspects than the push notifications areas. So I've been reading as many devs posts about ServiceWorkers as I could and decided... lets do this. So here's me dipping my toes into it, little bit of resources I've used, and one of the problems I ran into and it's solution.

Resources!

First there is already plenty of resources on getting started. So no need for me to reinvent the wheel. These few below are what helped me wrap my brain around it, and see the code in action instead of just random code on a page. By no means is this all there is or all inclusive. Its a springboard.

Tips!

For my personal site I run it in a virtual host on my computer. That is I use corydowdy.dev instead of 'http://localhost/corydowdy'. For everyday development this isn't an issue. It helps keep the paths pretty much the same for my cms in production. Service workers require HTTPS. So your dev environment will need to use HTTPS for Service Workers. Which means you may need to have a cert or create a self signed one.

If you plan on using any "powerful features" in the near future you'll need HTTPS. Things like:

  1. getUserMedia
  2. geoLocation
  3. fullScreen API
  4. Device Orientation and Motion

If you feel like "making a self signed cert" is making you jump through hoops to develop then there are some solutions when it comes to Service Workers.

Solution 1

Using Firefox Developer Edition or regular Firefox you can enable Service Workers over HTTP while the developer console/toolbox is open.

  1. Open Developer Toolbox. Either right click inspect element or:
    • ctrl + shift + i for Windows & Linux
    • cmd + opt + i for Mac
  2. Navigate to the settings cog on the right. Highlighted in the image.
  3. Under "Advanced" check "Enable Service Workers over HTTP (When Toolbox is open)"
Firefox Developer Toolbox Enabled Service Workers Checkbox

Checkbox for Service Workers checked.

Here is a hastily made screencast of what happens when the toolbox isn't open over HTTP followed up by enabling Service Workers over HTTP and leaving the developer toolbox open. You can see in the console the service worker trying to register then give you the warning it has failed. Upon enabling Service Workers and refreshing the page the "registration failed" warning now says "service worker registration complete". It works!

Solution 2

If your main development browser is Chrome and your dev site is simply http://localhost or http://localhost/site-here you're free to get on developing. If its in a virtual host, ie site.dev then this command line switch will let you develop your ServiceWorker script over regular insecure HTTP.


./chrome --user-data-dir=/tmp/foo --ignore-certificate-errors --unsafely-treat-insecure-origin-as-secure=http://your-site.dev

As it stands right now. Chrome is a bit easier to develop and debug Service Workers in even if you consider the command needed to use Service Workers locally under a non localhost url. Chrome allows you to check a box to "update on reload" and delete a Service Worker cache from the dev tools. Not to mention the network throttling and offline tools which Firefox says they will soon add as well.

Firefox you can navigate to about:serviceworkers to see the Service Workers that are registered.

Preventing ServiceWorker From Being Cached

In your dev environment it'll be preferable that your Service Worker script to not be cached. This way updates to the script can be reflected in the browser. These are strictly for development purposes. Remove in production.

Prevent Caching in Apache.


<Files "sw.js" >
  <IfModule mod_headers.c>
    Header unset ETag
    Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
  </IfModule>
</Files>

Prevent Caching in Nginx


location = /sw.js {
  expires -1; # immediately expire this file
  add_header Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
}

Critical CSS Workflow

I use a Critical CSS workflow and a script from the Filament Group called Enhance. This takes some of their functions, cookie, getMeta, loadCSS and loadJs, and packages them up into a nice script.

They recomend in their examples of using Server Side includes. I use twig. An example of this would be...

Server Side includes - Apache


<!--#if expr="$HTTP_COOKIE=/fullcss\=true/" -->
    <link rel="stylesheet" href="stylesheet-here.css">
<!--#else -->
    <style>
        /* critical CSS goes here*/
    </style>
<!--#endif -->
    <script>
        <!--#include virtual="/path/to/enhance.js" -->
    </script>
    <noscript><link rel="stylesheet" href="stylesheet-here.css"></noscript>

Twig ( or PHP ) pattern


{% if app.request.cookies.has("fullcss") %}
    <link rel="stylesheet" href="stylesheet-here.css">
{% else %}
    <style>
        /* critical CSS goes here*/
    </style>
{% endif %}
    <script>
        {% include "/path/to/enhance.js" %}
    </script>
    <noscript><link rel="stylesheet" href="stylesheet-here.css"></noscript>

With many of the examples listed above in Resources they recommend you cache your homepage or your blog page in the initial installation of the ServiceWorker like so:


self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my-cache::v1')
      .then( cache => {
      cache.addAll([
        '/blog',
        '/js/javascript-needed-diff-page.js',
      ]);
      return cache.addAll([
        'global.css',
        'global.js',
        '/',
        '/images/img.jpg'
        // etc
      ]);
    })
  );
});

On initial install both my homepage ('/') and my blog page ('/blog') are cached. This is great for perf and almost near instant loading.

ServiceWorkers will throw a kink into this because on a repeat visit or a refresh (depending how your serviceworker is set to cache) nothing will touch the server. The calls for the server side include or the twig check for the cookie won't produce anything. On a desktop this isn't an issue because my critical css has all the needed CSS to render the page. On a small screen device there isn't an issue either....until you try to open the navigation. Here is an example.

Every time I return to the homepage or the blog page or refresh those pages and press the "menu" button nothing happens. The menu button Javascript works but the CSS isn't applied.

Since the blog page and the homepage were cached on install the server has no chance to respond to the cookie set. Neither server side includes nor the check in twig for the cookie will succeed.

This could be remedied by including the dropdown css in the critical css. This though gets me though right at or just below the 14kb budget I have set for inlined critical css.

Luckily for me as I was starting down the ServiceWorkers path the Filament Group updated the loadCSS function to use the resource hint preload .

Using the new pattern with preload supplied by the Filament Group it removes the Server Side Includes or server based cookie check like I have above with php/twig.

Your templating language will probably differ from the twig template I have below but the concept is the same across templating languages.


{#
  Still have the Cookie check for browsers that don't support ServiceWorkers
#}
{% if app.request.cookies.has('fullcss') %}
  <link rel="stylesheet" href="{{ paths.theme }}css/global-styles.min.css"/>
{% else %}
  {# No 'fullcss' cookie present then load the inline styles #}
   {% block inlined_styles %}
   {# Individual Inlined Styles Go in Page Template #}
   {% endblock %}
   {# use loadCSS along with Preload to load the stylesheet asynchronously and not block rendering #}
  <link rel="preload" href="{{ paths.theme }}css/global-styles.min.css" as="style" onload="this.rel='stylesheet'">
  {% block enhance %}
  <script>

  {# Include Enhance JS script inline in our page  #}
  {% include 'js/enhance/enhance-script.js' %}

  </script>
  {% endblock enhance %}
{% endif %}
{# fall back styles for no javascript #}
  <noscript><link rel="stylesheet" href="{{ paths.theme }}css/global-styles.min.css"/></noscript>

LoadCSS also contains a polyfill for 'link rel="preload"' so it will use the standard loadCSS function in browsers that don't support preload, which currently only Blink based browsers do - Chrome and Opera.

With the above pattern now applied to my site it will function as follows:

  1. Install service worker if its supported and cache the homepage and blog page along with global css and js
  2. Set a cookie to load the full css on a return visit
  3. In Service worker instances use preload and the preload polyfill to load the full css bypassing the cookie check

Now the full css file is loaded on a return visit to our homepage and the navigation works again!

Benefits

So is adding a service worker beneficial to this sites perf? Well... yeah. Return trips really benefit from the service worker cache on a cable connection. Initial first visits don't see much variance on any connection type.

Webpage Test Results With and Without Service Worker
First Byte Start Render Speed Index
Browser First View Repeat View First View Repeat View First View Repeat View
Chrome Without Service Worker 0.354s 0.222s 0.873s 0.674s 902 702
With Service Worker 0.280s 0.776s 0.794s 0.323s 804 303
FireFox Without Service Worker 0.284s 0.226s 0.689s 0.391s 701 400
With Service Worker 0.356s 0.308s 0.746s 0.319s 703 300

Where this particular Service Worker really shines in on a 3G or slower connected device.

Webpage Test Results With and Without Service Worker Over a 3G "Slow" Connection
First Byte Start Render Speed Index
Browser First View Repeat View First View Repeat View First View Repeat View
Chrome Without Service Worker 1.754s 1.538s 2.390s 1.184s 2390 1895
With Service Worker 1.749s 0.098s 2.093s 0.298s 2183 305

The Service Worker also seems to be playing a trick. In the above table showing Chrome and Firefox with a service worker installed there seems to be a discrepancy with repeat views and the start render times and first byte in Chrome.

Browser First Byte Repeat View Start Render Repeat View
Chrome 0.776s 0.323s

The "first byte" is a lot slower than the start render. Since we have a Service Worker installed I'm going to assume that the first byte is actually the response time from Typekit serving my fonts since they are not cached with the service worker. Here is the filmstrip showing the start render at .3 seconds which gives us our start render of 0.323 seconds.

And Now A Jumbled Wrap Up

Should you use Service Worker? If you have an HTTPS enabled site yeah! Do it! The Service Worker won't register or run the code on browsers that don't support it so browser support isn't a big hurdle. If you have HTTPS you're probably using HTTP/2 so might as well throw this performance increase in too. Visitors with a slow connection will benefit greatly and visitors with a good connection will be just as happy with a fast loading site.