Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline support with service worker #978

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions _includes/header.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<input type="checkbox" id="babel-toggle-search" class="babel-toggle-search-checkbox" />
<header class="navbar navbar-default navbar-fixed-top babel-nav" id="top" role="banner">

<div id="offline-indicator" class="hidden">
You are offline
</div>

<div class="container">
<a href="/" class="navbar-brand logo">Babel</a>
<div class="babel-navbar-toggles">
Expand Down
1 change: 1 addition & 0 deletions _sass/_main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ body {
@import "components/_tick-list.scss";
@import "components/_type.scss";
@import "components/_user.scss";
@import "components/_offline-indicator.scss";

@import "pages/_404.scss";
@import "pages/_blog.scss";
Expand Down
4 changes: 4 additions & 0 deletions _sass/components/_offline-indicator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#offline-indicator {
text-align: center;
padding: 5px;
}
22 changes: 22 additions & 0 deletions scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,25 @@ $(document).ready(function() {
}]
});
});

if ('serviceWorker' in navigator) {

navigator.serviceWorker.register('/service-worker.js').then(function() {
var offlineIndicator = $("#offline-indicator");

window.addEventListener('online', function() {
offlineIndicator.addClass("hidden");
});

window.addEventListener('offline', function() {
offlineIndicator.removeClass("hidden");
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly leaving here as a note in case helpful later :)

The navigator.{online/offline} events may not accurately indicate that you can or can't access the network. There are well documented gotchas where you might need additional means to check that you're really online. One is checking connection loss by making failed XHR requests (retry a request a few times, if it doesn't go through, you're definitely offline).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addyosmani flipkart is awesome !

if (!navigator.onLine) {
offlineIndicator.removeClass("hidden");
}

}, function() {
console.log('CLIENT: service worker registration failure.');
});
}
16 changes: 16 additions & 0 deletions scripts/sw-toolbox.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
---

importScripts('/scripts/sw-toolbox.js');

const VERSION = 1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will you make sure that VERSION is incremented whenever any existing content anywhere on the site is updated? If VERSION doesn't get incremented, then the previously cached content for a given URL will never get refreshed, since there's a cacheFirst policy being used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sure. We could use the site.date from Jekyll. But this way the entier cache will be invalided even if the content hasn't changed.


toolbox.cache.name = "Babel-Cache-" + VERSION;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything here that will delete old caches once this cache name changes. That means that you'll end up with many copies of your full site in the SW site, each time you change the name.

Normally, you'd want to clean up unneeded caches inside an activate handler.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to Jeff's comment. You'll want to iterate through your cache entries and cache.delete doing something along these lines:

// Where urlsToCacheKeys is a map of your items to cache
self.addEventListener('activate', function(event) {
  let setOfExpectedUrls = new Set(urlsToCacheKeys.values());
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.keys().then(function(existingRequests) {
        return Promise.all(
          existingRequests.map(function(existingRequest) {
            if (!setOfExpectedUrls.has(existingRequest.url)) {
              return cache.delete(existingRequest);
            }
          })
        );
      });
    }).then(function() {
      return self.clients.claim();
    })
  );
});

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than cleaning manually the cache, I think we need to limit the cache using cache.maxEntries or cache.maxAgeSeconds.
The old cached website(s) will be cleaned up given these configurations.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be good to use a versioned cache name.

const VERSION = 1;

toolbox.cache.name = "Babel-Cache-" + VERSION;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also,

toolbox.cache.maxEntries = 20 // or 15

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will add cache versioning.

What do you think about a max age ? Like 7 days ?

I don't know if limiting entries is a good idea. I pre-cache already all pages.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL for sw-toolbox and sw-precache here. Great to see you using it :)

If you're going to set maxEntries to 20, it means that after the 21st entry is cached, the least-recently used entry would be automatically deleted. Unless you're storing a number of really large assets, I'd consider whether this is really that valuable. I don't think the Babel site would benefit from setting this all that much.

I do think there's use in imposing a maxAgeSeconds age. @boopathi can probably comment to the length of time, but a week doesn't sound like a bad ballpark.

var preCachedRessources = [
{% for page in site.pages %}
'{{ page.url }}',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're precaching entire HTML documents, then you're going to end up populating user's caches with a lot of duplicated data. I.e. if your HTML shares common headers/footers (because they're all using some underlying layouts) then the structure of those headers and footers will be take up space on your user's disk when they don't actually add any extra value.

The ideal implementation would just cache the layout elements once, then cache the underlying content once, and then perform the templating logic in the service worker to combine them at runtime.

I've got a very experimental example of this implemented at https://github.com/jeffposnick/jeffposnick.github.io/tree/work/src, but I don't know that it's stable enough to recommend using for a high-traffic site.

{% endfor %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the following is valid. I see this to be valid at-least for page transitions from and to REPL.

The Service Worker shouldn't change from pageA to pageB if both the pages are under the scope of the same service worker. This is because, service worker updates based on the byte diff, and during a page change, a new service worker will install (if the resources are different -> i.e new service-worker.js).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the strategy is cacheFirst I removed the file main.css which has a time in it.

];

toolbox.precache(preCachedRessources);

toolbox.router.get('/*', toolbox.cacheFirst);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super broad. Are you sure you're okay with every URL under your origin being served cacheFirst? And aside from the caching strategy, are you sure that any time there's any request made for a resource on your site, you want that added to the cache? If there are large images or other large media, then that's not necessarily a good approach.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted an offline documentation. This caching strategy might indeed not be the best approach.

toolbox.router.get('/*', toolbox.cacheFirst, { origin: "cdnjs.cloudflare.com" });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd probably want to configure a different cache name here the runtime CDN caches, so that they're reused indefinitely and don't become irrelevant whenever you change the default toolbox.cache.name value above.

If you're using cacheFirst strategy for them, please make sure that all of the URLs that they refer to are versioned. E.g. https://unpkg.com/[email protected]/dist/react.min.js includes 15.3.1 in the URL, so it's fine to use cacheFirst. But if you request URLs like https://unpkg.com/react/dist/react.min.js, once it's cached for the first time, the initial version will be reused indefinitely (at least until the cache name changes).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good point. This is part of good practice I guess.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked, resources from CDN (Cloudflare and jsdelivr) have a version in the URL.

toolbox.router.get('/*', toolbox.cacheFirst, { origin: "cdn.jsdelivr.net" });
toolbox.router.get('/*', toolbox.cacheFirst, { origin: "unpkg.com" }); // for repl
toolbox.router.post('/*', toolbox.cacheFirst, { origin: "algolia.net" }); // Cache Algolia search response

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this in your earlier commits, but is the goal here to always return a cached post response from Algolia or to only have this work when the network is available?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep search even once offline. The goal here is to cache some responses from Algolia because it does not support offline searching. I'm not sure about this since the user will be able to find what he already searched online.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addyosmani I guess not. it should be networkFirst.

@xtuc cacheFirst, networkFirst, fastest - all these ultimately hit the cache in some way.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know exactly what algolia.net is used for specifically, but if it's search-related, then cacheFirst doesn't sound like a good strategy to use. networkFirst would normally be more appropriate, since you'd want fresh search results, but could fall back to stale results if the network is unavailable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

algolia.net is for the search functionality

screen shot 2016-11-15 at 11 00 34 am

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm now thinking that idea wasn't that good. (#978 (comment))