RFC: Default ServiceWorker to avoid cascading invalidation
See original GitHub issueBackground
Currently in v2, we include a content hash in filenames by default (except entries). Since #3414 this has also included all child bundles as well. This is necessary because the URLs of child bundles are in parents in order to load them, but it means that if a child bundle changes, all of its ancestors must be invalidated, negating any caching benefit. We discussed some possible solutions to this problem, and @philipwalton’s recent article also brought this back into our minds.
Service Workers
The best solution seems to be to use a Service Worker to handle the caching instead of the browser’s HTTP cache, and avoid content hashes in filenames entirely. This means that the filenames will not change between builds, so invalidating a child bundle does not invalidate its parents. In order to handle caching, a Service Worker with a manifest will be generated including hashes for all of the bundles. The Service Worker will request assets from the network, and cache them using the hash from the manifest as a cache key. Whenever the app is deployed, the manifest will be updated, and only the bundles that changed will need to be downloaded since they aren’t in the Service Worker’s cache.
Service workers have one major problem though: They break the browser reload button. If you serve cache first, the service worker will not be updated until AFTER the page has been reloaded and the user is already seeing stale content. By default, the new service worker won’t even be activated until the user closes and reopens all of their tabs for that website either. This can be mitigated by self.skipWaiting()
but even then you still need 2 reloads.
The solution I came up with for this is to put the manifest in a separate tiny JSON file parcel-manifest.json
that would be deployed alongside the app. The service worker would load this file as part of its installation step, and on top-level page navigation events. This way, the service worker would not change between builds, only the manifest would. This avoids the double reload problem because the service worker would get the refreshed manifest prior to reloading the page.
It would look something like this:
parcel-manifest.json
{
"index.html": "48f7a7cb",
"index.js": "6fbe6e5b"
}
service-worker.js (pseudocode)
self.addEventListener('install', event => {
// fetch and cache manifest
});
self.addEventListener('fetch', event => {
if (!manifest || event.navigation.mode === 'navigate') {
// update manifest
}
let cacheKey = getCacheKey(manifest, event.request);
if (cache.match(cacheKey)) {
return event.respondWith(cached);
}
let res = fetch(event.request);
cache.put(cacheKey, res);
event.respondWith(res);
});
The downside here is one extra network call for the manifest on each navigation. However, I believe this is still overall better than the current situation with no service worker at all, and it works according to user expectations. Most of the time, you only need to pay the cost of one tiny network call to get the manifest (your entry HTML is now cacheable!), and if anything is invalidated, only the bundles that changed are downloaded instead of all of its ancestors as well. In addition, the app would work offline by default since we’d return cached content using a cached manifest, and we could do the same after a short timeout for lie-fi situations as well.
Fallback
For fallback on old browsers that don’t support service workers, we’d connect this feature to ES module output by default since they have very similar support matrices. If you use <script type="module">
you’d get service worker caching with no content-hashed filenames, while normal scripts would continue to be content-hashed as they are today.
Feedback
Please comment with your feedback! It would be greatly appreciated to hear lots of perspectives on this.
Issue Analytics
- State:
- Created 4 years ago
- Comments:19 (11 by maintainers)
I’m just going to quickly throw out there that in Parcel 2 I believe this can all be worked out in plugin land. One of the main motivations of making so many things in Parcel exposed as plugins was to enable the community to experiment with ideas like this.
So I would propose this RFC be implemented as “experimental” plugins that we could build out and have audited by everyone interested. I’d be happy to put an app in production with a fairly-stable-but-still-experimental plugin and give feedback.
FWIW, an example of a service worker that shares conceptual similarities with what Jake describes can be found as part of this AppCache Polyfill library.
One difference is that in that project, IndexedDB is used to share manifest info between client pages and the service worker, keyed on client ID, rather than
postMessage()
.Notably, this approach only works if you add code to both the
window
client and service worker.(It’s fair to say that this implementation also makes compromises on the “preventing performance regressions” side of things, but fidelity with the AppCache specification was prioritized over performance.)