For years, service workers have promised us a feeling of a native app straight in the browser. While specific parts of it are true, such as access to device hardware (gyroscope, orientation sensor, etc.) or background sync, we're still far cry from the native feel in areas such as push notifications, which don't work at all in iOS, and especially offline support. While it's fun to have options and experiment on private, small projects, we can't really consider technologies to be production-ready when they don't support half the devices out there.
With that in mind, service workers also come with a lot of baggage. And we've found that out in a hard way.
Problem with service workers
First, you start developing your app, include service worker because it's a cool new thing to do, you want offline support or definitely want to have all the stuff they bring as an option to use. Months go by, you release v1 of the app and users start rolling in. As it's the first version, things are bound to change, so you implement the first couple of changes. You maybe change some copy around, some links, but soon you get a report that you have a huge bug discovered by one of your users. Maybe something potentially catastrophic for your database, it happens.
Fear not, you patch it up and go about your day. Months go by, and you release v2 of the app, market it like crazy and it reaches some top lists of this and that, and same users that entered your app a couple of months ago, and never again, land on your page and load a completely cached version from the last time they visited, together with any hardcoded values, bugs, funnel changes... essentially a snapshot from the past.
By the time you realize what you have done, you've already saved a snapshot of the app in the arbitrary time to thousands of devices, unable to wipe the cache on their devices, and just sit and wait for them to potentially open your older, less secure version of the app from months ago.
The way service worker works is when users land on your website for the 2nd time, it loads all assets and files from SW and THEN checks if you have pushed a new SW update in the meantime. If it finds one, it schedules it to be loaded next time you open the website. To make matters worse, normal refresh won't load the new SW content, but you have to close the session (i.e. close all website tabs currently open, or whole browser) and reopen the website to load the new version.
By following this article:
Let Users Know When You Have Updated Your Service Workers in Create React App
Gert Glükmann ・ Jan 4 ・ 5 min read
I found out that you can insert an action into this process of discovering the new version of SW. The problem is that you can either notify users that a new version is available and leave it up to them to click Load new version which will reload the website properly, or you can force a refresh when the browser figures out that a new SW version is available and installed, which can be after a couple of seconds and well into the time when the app was already interactive, meaning that user started to do something.
Usually, early in the live of an app, updates could be released several times a day, which means users could get a prompt or hard refresh multiple times a day when they land on a website, which is also not a good option.
Other way around, we leave ourselves open to the ghosts of the past by giving users the option to load a new version of the site or not. This way malicious users could take advantage of the app in its pre-patched state.
The worst thing is, when you decide to change your approach, you still have all the old users cached on your previous decision and they don't get the memo that they should reload as soon as they open the site.
Now, when you think about it, it becomes obvious that this is how native apps work from the start. You have a version that is the latest and a lot of previous versions that people have installed, and are still using. But the web isn't native.
As leob mentioned in the comments (thanks!) some apps force users to update to new versions even in the native environment. Banking apps first come to mind, which just confirms that the issue exists and if security is an important priority for us, that we have to address it in a creative way.
Many years ago, Web solved the issue where users had to install a new version of software on their devices, where we had to incentivize them to update or upgrade, and support legacy versions with both features and bugfixes. We approach web development with a different mindset than native development. We KNOW we can push updates very quickly to 100% percent of our users, and we are used to pushing experimental features, knowing very well that we can patch them up as we go or remove them fairly quickly if they turn out to be bad ideas. Including service workers turn the tables around once again, by introducing fragmentation, legacy support, and fear of the unknown on the internet, which is something that should be avoided at all costs.
Sure, if we create just a couple of versions all of which are perfect, this is a non-issue, but let's be real, those apps don't exist. So how do you deal with these issues if you still want to keep offline support? Do you use service workers at all? Let me know!
Top comments (8)
Great points, that sounds like an instance of "have your cake but not be able to eat it" ... I don't have real hands-on experience with this kind of stuff, but it sounds like you really have to force your users to upgrade, at least in case of "major" upgrades, I think there are also native apps which do that:
"Sorry, but your app version is incompatible with our services, please click HERE to upgrade ..."
So, on app startup, but also at strategic places after that, I'd do a version check "is this frontend app compatible with the current backend API" and then show a link or button which they have to click to upgrade. Make it mandatory only for "major" upgrades.
That's a good point, I wanted to touch on that but forgot. Updated in the article, thanks :)
Issue is that you HAVE to have event that check 100% done right in the first version that hits the web, otherwise you risk caching users to the version without the check. That's the issue actually - when you release a major upgrade, you are not sure that everyone is seeing this upgraded app when they are opening the app for the first time after some time.
Haha that's absolutely right, that check has to be there and working perfectly in version 1.0 of your app ... I think this should be mentioned in EVERY article or tutorial that touts the benefits of PWAs. And you're right that an "old fashioned" web site (not a 'PWA' or an 'app') doesn't have these issues!
While I don't have much experience in this, I think a conservative approach would work here:
While I agree with your process, caching static assets is solved years ago with server-side caching and cache busting works flawlessly for serving new content when it's available without falling back to old ones first. The issue with offline support for SW is that it's all or nothing, and not caching specific assets. That should be handled by the server, not the browser, either way. Let me know if I'm understanding something wrong about SW config :) Thanks for the comment!
Caching using headers is indeed a breeze, but it falls short in two scenarios:
Service workers offer a lower-level caching mechanism that can bypass these limitations.
That said, if you can control your headers and don't plan on offline access (but are caching for perf improvement), headers are the way to go.
I don't know how Adobe's PDF viewer, and Google Chrome do this but; it's all silently done in the background unless we turn it off. I feel following this solution is the best way forward.
Any links for the approach you're talking about? Thanks!