Frontend: Service worker caching strategy

Created on 13 Aug 2017  路  10Comments  路  Source: home-assistant/frontend

So with the switch to Polymer-build instead of my custom build pipeline, a couple of dependencies that used to be in panels only, got moved to be part of the app-shell bundle.

Due to the way our caching strategy works, this has caused issues for people who are using the service worker (Chrome users serving Home Assistant over HTTPS).

The current scenario is bound to happen again if we don't fix it. Would love some insight in how we should approach this.

CC @andrey-git @armills

The scenario where a new frontend can break when using service workers

We fingerprint our assets to differentiate between versions.

First load experience

  • Index HTML is served by the webserver
  • Loads app shell bundle
  • Load panel urls via API call
  • Load panel with url we got from API call

Second load experience of same version without service worker

  • Index HTML is served by the webserver
  • Loads app shell bundle from cache
  • Load panel urls via API call
  • Load panel from cache with url we got from API call

Second load experience of same version with service worker

  • Index HTML is served by service worker
  • Loads app shell bundle from service worker cache
  • Load panel urls via API call
  • Load panel from service worker cache with url we got from API call

Second load experience of new version without service worker

  • Index HTML is served by the webserver
  • Loads app shell bundle for v2
  • Load v2 panel urls via API call
  • Load v2 panel with url we got from API call

Second load experience of new version with service worker

  • Index HTML v1 is served by the service worker
  • Loads app shell bundle for v1
  • Load v2 panel urls via API call
  • Load v2 panel with url we got from API call
  • BUG: If a dependency has been moved to app shell bundle v2, the v2 panel will not have all it鈥檚 dependencies loaded.
service worker

Most helpful comment

Found this issue after reading through #110.

It seems we also have the scenario where the service worker loads everything, but the API call fails (typically with 302 Found for external authentication, or 401 Unauthenticated for basic auth).

Without too much knowledge on how service workers work, it sounds to me that the cache should be invalidated in this case as well.

All 10 comments

Solution 1: store the panel urls in the Index HTML.

This way we will load the same versions of the panels as is our app shell bundle.

This has the downside that if you enable a new component that has a new panel, you will not be able to see it until the service worker refreshes.

I guess we can store a hash of all known panels in the index and no longer store urls in the API response. If we have to fetch a panel that is not part of PANELS we will force a refresh of service worker + frontend.

PANELS = {
  'map': 'abcdefghkjilkjm',
  'history': 'zxcjdijasdiojads',
}

Solution 2: fetch our app-shell url from the API

Bad idea, would kill all performance optimizations that we have done.

Solution 3: Service worker version checking

Check in service worker if frontend is trying to fetch a panel that we don't have in our cache, if so, delete itself, force refresh of the page and have new service worker take over.

General thoughts: My preference is that we should limit ourselves to solutions that will serve v2 files. Even though we could create solutions that serve a consistent set of v1 files, I think it will lead to more bug reports.

Solution 4: Attempt bypass of service worker for index.html

If we provide a last-modified header on our index.html response, we can then implement a 304 response when it's requested in the future. We would probably want this last modified to be generated by script/build_frontend and stored in FINGERPRINTS. We could then possibly perform this request from inside of the service worker, and still be able to fall back to the cached version if hass is offline.

The problem with attempting to bypass the service worker is that we're slowing down startup of Home Assistant significantly. People will be looking at a white screen if their phone is on a flaky internet connection.

Solution 5: Force refresh to use new service worker

The v2 service worker will be installed while index.html served the v1 frontend. We can force a refresh of the frontend to have it load v2 index.html.

You will see a similar solution on a lot of pages that use service worker, but instead of forcing a refresh they will show a toast in the bottom left: "A new version is available. UPDATE" (screenshot)

https://medium.com/@zwacky/pwa-create-a-new-update-available-notification-using-service-workers-18be9168d717

I think given the constraint that we always want to be able to display straight from cache, Solution 1 would be the best. We could possibly combine it with some API layer notification to refresh like listed in Solution 5, but even without that we'd avoid the current bugs.

We should optimize for "nothing changed" version as it happens much more often.

A problem with solution (1) i.e. serve old panels, is that that they could be backward incompatible with the server.

Why aren't we keeping everything - including panels in SW? So that the user can view "last state" even if backend is unavailable?

So maybe
1) Load shell offline via SW. Load panels as they are loaded now.
2) Make an API call with UI version as a param.
3) Backend responds with either of OK / Propose a reload / Force reload

We keep everything already in SW, it's just that the urls we fetch from in case of a new version don't match the cached versions and thus hit the server. Solution 1 would be moving towards keeping everything in SW.

Some more brainstorming as I'm thinking about this: Solution 1 ensures that we can always show a cached UI with consistent JS. As @andrey-git notes, there's a possibility that the backend API could change, but that's at least better than complete JS breakages. The next question is how can we invalidate the SW cache when a new version of hass backend is loaded, to reduce the impact of those incompatibilities.

An API call could definitely work, but IMO it's introducing some smell of mixing API with client assets. One possibility is something along the lines of Solution 4. Not sure of the full capabilities of the SW but this is what I'm thinking: After the SW serves the app to the client, we could attempt an online request of index.html in the background. If we get a 304 response, we know no action is required. Otherwise if a new index.html is received the SW can update it and invalidate the rest of the cache. The added benefit of this is that clients without a SW can make use of a cached index.html, even if it still requires a network request. This ends up behaving similar to an API call, but we're making use of the exiting HTTP protocols for cache checking.

Found this issue after reading through #110.

It seems we also have the scenario where the service worker loads everything, but the API call fails (typically with 302 Found for external authentication, or 401 Unauthenticated for basic auth).

Without too much knowledge on how service workers work, it sounds to me that the cache should be invalidated in this case as well.

This is no longer an issue because all panels are now bundled with the frontend and custom panels are URL based.

Was this page helpful?
0 / 5 - 0 ratings