AllanChain / blog

Blogging in GitHub issues. Building with Astro.
https://allanchain.github.io/blog/
MIT License
13 stars 0 forks source link

Gridsome is not that Ready for PWA #149

Open AllanChain opened 4 years ago

AllanChain commented 4 years ago

View Post on Blog

How I am tring to build a nice gridsome-generated site with PWA support

虽然可以勉强并入“用 Vue 做 PWA”系列,但想着能让更多人看到,就还是用国际语言吧


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:

  1. user browse version A of the page
  2. version B published
  3. user browse again, no change (sw returning staled data and revalidating)
  4. 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:

  1. user browse version A of page I (site version: A, page I version: A)
  2. version B published
  3. user browse version B of page II (site version: B, page I version: A, page II version: B)
  4. user goes offline
  5. 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?

milindsingh commented 3 years ago

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 ?

AllanChain commented 3 years ago

@milindsingh I am not having this issue 🤷‍♂️ As reference, you can try gridsome branch of this repo and comment out PWA plugin section in gridsome.config.js for simplicity. For example axios is loaded when the page needs it.