Preface: Why I Need PWA #
More than 300 KB of vendor chunks (vue
, vuex
, vue-router
, vue-meta
, vuetify
, etc.) with a not-that-fast network is very slow to load. I really don't want it to happen again. Also if the page service is down or unreachable (does happen from time to time, at least for my region), I want the site still to work properly.
First of All: Optimise Assets #
If your sites updates frequently, you need to change the default behavior.
Cache Busting #
By default, gridsome write hashes to page html and json file. While it helps controlling versions, the hash itself is unreliable. That's becuase the hash is generated by webpack build, which is somewhat random. I am even getting different hashes when building gridsome.org
, without changing a single character!
Also, if the hashes do work properly, I don't want to invalidate all cached data just because I change one byte of javascript code.
So I jut turned cache busting off:
{
cacheBusting: false
}
But I still don't want browser to load old assets for new HTML. So I turns on assets hashing manually:
api.chainWebpack(async (config, { isClient, isProd }) => {
if (isProd && isClient) { // if splitting CSS
config.plugin('extract-css').tap(() => [{
filename: 'assets/css/styles.[contenthash:8].css'
}])
config.output.filename('assets/js/[name].[contenthash:8].js')
config.output.chunkFilename('assets/js/[name].[contenthash:8].js')
}
}
Splitting Chunks #
By default, gridsome packs main.js
, App.vue
, etc., and used node modules into one app.hash.js
, which is more than 300 KB in my case, basically vuetify and gridsome with modules it depends on. Changing one byte of App.vue
means downloading all 300 KB again, which is a pretty awful UX.
Let's split the chunks:
api.chainWebpack(async (config, { isClient, isProd }) => {
if (isProd && isClient) {
config.optimization.splitChunks({
chunks: 'initial',
maxInitialRequests: Infinity,
cacheGroups: {
vueVendor: {
test: /[\\/]node_modules[\\/](vue|vuex|vue-router)[\\/]/,
name: 'vue-vendors',
},
gridsome: {
test: /[\\/]node_modules[\\/](gridsome|vue-meta)[\\/]/,
name: 'gridsome-vendors',
},
polyfill: {
test: /[\\/]node_modules[\\/]core-js[\\/]/,
name: 'core-js'
},
axios: {
test: /[\\/]node_modules[\\/]axios[\\/]/,
name: 'axios'
}
}
})
}
}
I can only split out 1 chunk if maxInitialRequests
is not set.
Note: The above code overwrites gridsome's css: { split: false }
config and always splits CSS. To disable CSS splitting, add these lines:
styles: {
name: 'styles',
test: m => /css\/mini-extract/.test(m.type),
chunks: 'all',
enforce: true
}
Choosing Cache Strategy #
Precache v.s. Runtime Cache #
In short, precache caches all files specified in the manifest, which is usually a list of js and css assets, at install time.
Runtime cache is more flexiable and have a lot of strategies to choose.
My Best Practice Now #
Precache the assets, use NetworkFirst
strategy for HTML pages and post data, and CacheFirst
for images. Don't worry if the network is slow and still taking a long time to load, just set networkTimeoutSeconds
.
Why not... #
StaleWhileRevalidate
for JSON Data Files
#
The user will not receive changes immediately:
- user browse version A of the page
- version B published
- user browse again, no change (sw returning staled data and revalidating)
- user browse again, page changed
Also, caching JSON data means https://github.com/gridsome/gridsome/issues/1032#issuecomment-632908706
Add Revision to JSON Data Files #
Yes, this is posible by using injectManifest
and precache some of them, while runtime caching with a handler to add revision to the url. (Detailed implemantation)
However, compiling service worker with webpack is currently limited that I cannot split chunks with it. That means if the post data changed one byte, the user have to redownload the service worker file again, which is about 50 KB. Even if I managed to split the service worker, that's still 13 KB of manifest and will grow overtime. That's quite annoying that the service worker is always updating, slowly.
Of course if you are sure your users' network is fast, you can ignore above drawback.
Just Precache all JSON Data Files #
Precaching all data files has the same drawback as above. Plus, precaching all data files means the user is downloading the whole site on first visit, which is a horrible UX, as well as large network overhead. And only after precache is done will the service worker complete installing.
For example, first visit to https://v3.vuejs.org/ takes a long time to complete. ~400 files to precache.
Of course if the users should be able to browse the full site while offline, and you do not care first visit loading and service worker installing time, go ahead with precaching.
Still Problem #
Though using NetworkFirst
, sw will still use the cached JSON data version if offline. And the cached file may have different versions. For example:
- user browse version A of page I (site version: A, page I version: A)
- version B published
- user browse version B of page II (site version: B, page I version: A, page II version: B)
- user goes offline
- user browse page I, broken
Since the user is offline, it is expected that some pages are not available. Better to show freindly offline message instead of getting a wrong version of data and having the broken page. Maybe using the GraphQL query hash as part of JSON filename?
Thanks @AllanChain It really helped.
But I have few things, gridsome is still loading all chunks on the index page, but vuejs does page-wise.
Any idea how to only load relevant js bundle on the page ?