Using the VitePWA Plugin for an Offline Site | CSS-Tricks

Anthony Fu’s VitePWA plugin is a great tool for Vite-powered websites. Helps you add a service worker who handles:

  • Offline support
  • Store assets and content
  • Prompt the user when new content is available
  • … and other things!

We’ll get to know the concept of service workers together, and then go straight to creating one using the VitePWA plugin.

Are you new to Vite? Check out my previous post for an introduction.

Table of contents

  1. feet service workers

  2. Version and manifest

  3. Our number one service worker

  4. What about offline jobs?

  5. How are service workers updated

  6. A better way to update content

  7. Temporary run time

  8. Add your own service worker content

  9. wrapping

feet service workers

Before getting into the VitePWA plugin, let’s talk briefly about the service worker itself.

a service worker It is a background process that runs on a separate thread in your web application. Service workers have the ability to intercept network requests and do… anything. The possibilities are surprisingly wide. For example, you can quickly intercept and compile requests for TypeScript files. Or you can intercept video file requests and perform advanced transcoding that the browser does not currently support. Notwithstanding, the service worker is more commonly used to cache assets, to improve site performance and enable it to do so Something when it is offline.

When someone lands on your site for the first time, the VitePWA plugin service worker creates installs and caches all of your HTML, CSS, and JavaScript files by making use of the Caching API. The result is that on subsequent visits to your site, the browser will load these resources from the cache, rather than having to make network requests. And even on the first visit to your site, since the service worker Just Everything is pre-cached, so the next place the user clicks it will likely already be pre-cached, allowing the browser to bypass the network request entirely.

Version and manifest

You may be wondering what happens with the service worker when you update your code. If your service worker is caching, for example, a file foo.js file, and you modify that file, you want the service worker to pull the updated version, the next time the user visits the site.

But in practice, you don’t have a file foo.js a file. Usually the build system creates something like foo-ABC123.js, where “ABC123” is a hash of the file. If you update foo.js, the next posting may be sent to your site foo-XYZ987.js. How does the service worker deal with this?

It turns out that the Service Worker API is a file until far away Low level primitive. If you are looking for a native ready solution between it and the cache API, you will be disappointed. Essentially, your service worker creation should be automated, partial, and connected to the build system. You’ll need to see all the assets your build has created, encode those filenames into the service worker, have code to cache them beforehand, and most importantly, Keep track of Cached files.

If the code is updated, the service worker file also changes, which has the extension the new Complete file names with hashes. When the user makes his next visit to the application, the new service worker will need to install and compare the statement of the new file with the statement currently in the cache, and output files that are no longer needed, while the new content is cached.

This is a ridiculous amount of work and incredibly difficult to do right. While it can be a fun project, in practice you’ll want to use a well-established product to build your service worker — and the best product out there is Workbox, which comes from the folks at Google.

Even Workbox is kind of a low-level primitive. It needs detailed information about the files you cache, which are buried in your build tool. This is why we use the VitePWA plugin. It uses Workbox under the hood, and configures it with all the information it needs about the packages Vite creates. Unsurprisingly, there are also Webpack and Rollup plugins if you prefer working with those packages.

Our number one service worker

I’m going to assume you already have a Vite-based site. If not, feel free to create one from any of the available templates.

First, we install the VitePWA plugin:

npm i vite-plugin-pwa

We will import the plugin into our Vite configuration:

import { VitePWA } from "vite-plugin-pwa"

Then we use it in config as well:

Extras: [
  VitePWA()

We’ll add more options in a bit, but that’s all we need to create a surprisingly useful service worker. Now let’s register it somewhere in the entry of our application with this code:

import { registerSW } from "virtual:pwa-register";

if ("serviceWorker" in navigator) {
  // && !/localhost/.test(window.location)) {
  registerSW();
}

Don’t let the code that’s commented out throw you for a loop. It’s extremely important, in fact, as it prevents the service worker from running in development. We only want to install the service worker anywhere that’s not on the localhost where we’re developing, that is, unless we’re developing the service worker itself, in which case we can comment out that check (and revert before pushing code to the main branch).

Let’s go ahead and open a fresh browser, launch DevTools, navigate to the Network tab, and run the web app. Everything should load as you’d normally expect. The difference is that you should see a whole slew of network requests in DevTools.

A screenshot of DevTools listing all of the network requests for the currant app using the VitePWA plugin. There are a total of 16 various JavaScript and CSS files.

That’s Workbox pre-caching the bundles. Things are working!

What about offline functionality?

So, our service worker is pre-caching all of our bundled assets. That means it will serve those assets from cache without even needing to hit the network. Does that mean our service worker could serve assets even when the user has no network access? Indeed, it does!

And, believe it or not, it’s already done. Give it a try by opening the Network tab in DevTools and telling Chrome to simulate offline mode, like this.

Screenshot of the DevTools UO to simulate an offline connection with the select menu open. The No throttling option is currently checked but the Offline option is highlighted in light blue.
The “No throttling” option is the default selection. Click that and select the “Offline” option to simulate an offline connection.

Let’s refresh the page. You should see everything load. Of course, if you’re running any network requests, you’ll see them hang forever since you’re offline. Even here, though, there are things you can do. Modern browsers ship with their own internal, persistent database called IndexedDB. There’s nothing stopping you from writing your own code to sync some data to there, then write some custom service worker code to intercept network requests, determine if the user is offline, and then serve equivalent content from IndexedDB if it’s in there.

But a much simpler option is to detect if the user is offline, show a message about being offline, and then bypass the data requests. This is a topic unto itself, which I’ve written about in much greater detail.

Before showing you how to write, and integrate your own service worker content, let’s take a closer look at our existing service worker. In particular, let’s see how it manages updating/changing content. This is surprisingly tricky and easy to mess up, even with the VitePWA plugin.

Before moving on, make sure you tell Chrome DevTools to put you back online.

How service workers update

Take a closer look at what happens to our site when we change the content. We’ll go ahead and remove our existing service worker, which we can do in the Application tab of DevTools, under Storage.

Screenshot showing the Storage panel of DevTools. The DevTools menu is a panel on the left and the app usage is displayed in a panel on the right, showing that 508 kilobytes of data total is used, where 392 kilobytes are cached and 16.4 are service workers. A button to clear site data is below the Usage stats with a deep blue label and a light gray background.

Click the “Clear site data” button to get a clean slate. While I’m at it, I’m going to remove most of the routes of my own site so there’s fewer resources, then let Vite rebuild the app.

Look in the generated sw.js to see the generated Workbox service worker. There should be a pre-cache manifest inside of it. Mine looks like this:

A dark mode screenshot showing a list of eight asset urls inside of a precacheAndRoute function.

If sw.js is minified, run it through Prettier to make it easier to read.

Now let’s run the site and see what’s in our cache:

Let’s focus on the settings.js file. Vite generated assets/settings.ccb080c2.js based on the hash of its contents. Workbox, being independent of Vite, generated its own hash of the same file. If that same file name were to be generated with different content, then a new service worker would be re-generated, with a different pre-cache manifest (same file, but different revision) and Workbox would know to cache the new version, and remove the old when it’s no longer needed.

Again, the filenames will always be different since we’re using a bundler that injects hash codes into our file names, but Workbox supports dev environments which don’t do that.

Since the time writing, the VitePWA plugin has been updated and no longer injects these revision hashes. If you’re attempting to follow along with the steps in this article, this specific step might be slightly different from your actual experience. See this GitHub issue for more context.

If we update our settings.js file, then Vite will create a new file in our build, with a new hash code, which Workbox will treat as a new file. Let’s see this in action. After changing the file and re-running the Vite build, our pre-cache manifest looks like this:

Now, when we refresh the page, the prior service worker is still running and loading the prior file. Then, the new service worker, with the new pre-cache manifest is downloaded and pre-cached.

A DevTools screenshot showing a table of pre-cached assets processed by the VitePWA plugin and Workbox.
The new pre-cached manifest is displayed in the list of cached assets. Notice that both versions of our settings file are there (and both versions of a few other assets were affected as well): the old version, since that’s what’s still being run, and the new version, since the new service worker has pre-cached it.

Note the corollary here: our old content is still being served to the user since the old service worker is still running. The user is unable to see the change we just made, even if they refresh because the service worker, by default, guarantees any and all tabs with this web app are running the same version. If you want the browser to show the updated version, close your tab (and any other tabs with the site), and re-open it.

The same DevTools screenshot of pre-cached assets, but now only displaying new assets instead of duplicates.
The cache should now only contain the new assets.

Workbox did all the legwork of making this all come out right! We did very little to get this going.

A better way to update content

It’s unlikely that you can get away with serving stale content to your users until they happen to close all their browser tabs. Fortunately, the VitePWA plugin offers a better way. The registerSW function accepts an object with an onNeedRefresh method. This method is called whenever there’s a new service worker waiting to take over. registerSW also returns a function that you can call to reload the page, activating the new service worker in the process.

That’s a lot, so let’s see some code:

if ("serviceWorker" in navigator) {
  // && !/localhost/.test(window.location) && !/lvh.me/.test(window.location)) {
  const updateSW = registerSW({
    onNeedRefresh() {
      Toastify({
        text: `<h4 style="display: inline">An update is available!</h4>
               <br><br>
               <a class="do-sw-update">Click to update and reload</a>  `,
        escapeMarkup: false,
        gravity: "bottom",
        onClick() {
          updateSW(true);
        }
      }).showToast();
    }
  });
}

I’m using the toastify-js library to show a toast UI component to let users know when a new version of the service worker is available and waiting. If the user clicks the toast, I call the function VitePWA gives me to reload the page, with the new service worker running.

A toast component screenshot with white text and a slight background gradient that goes from light blue on the left to bright blue on the right. It reads: an update is available! Click to update and reload.
Now when we have pending updates, a nice toast component pops up on the front end. Clicking it reloads the page with the new content in there.

One thing to remember here is that, after you deploy the code to show the toast, the toast component won’t show up the next time you load your site. That’s because the old service worker (the one before we added the toast component) is still running. That requires manually closing all tabs and re-opening the web app for the new service worker to take over. Then, the next time you update some code, the service worker should show the toast, prompting you to update.

Why doesn’t the service worker update when the page is refreshed? I mentioned earlier that refreshing the page does not update or activate the waiting service worker, so why does this work? Calling this method doesn’t only refresh the page, but it calls some low-level Service Worker APIs (in particular skipWaiting) as well, giving us the outcome we want.

Runtime caching

We’ve seen the bundle pre-caching we get for free with VitePWA for our build assets. What about caching any other content we might request at runtime? Workbox supports this via its runtimeCaching feature.

Here’s how. The VitePWA plugin can take an object, one property of which is workbox, which takes Workbox properties.

const getCache = ({ name, pattern }: any) => ({
  urlPattern: pattern,
  handler: "CacheFirst" as const,
  options: {
    cacheName: name,
    expiration: {
      maxEntries: 500,
      maxAgeSeconds: 60 * 60 * 24 * 365 * 2 // 2 years
    },
    cacheableResponse: {
      statuses: [200]
    }}});  // ... plug-ins: [
    VitePWA({
      workbox: {
        runtimeCaching: [
          getCache({ 
            pattern: /^https://s3.amazonaws.com/my-library-cover-uploads/, 
            name: "local-images1" 
          }),
          getCache({ 
            pattern: /^https://my-library-cover-uploads.s3.amazonaws.com/, 
            name: "local-images2" 
          })
        ]
      }})]// ...

I know this is a lot of code. But all it really does is tell Workbox to cache anything it sees matching URL patterns. The docs provide more information if you’d like to dig into the details.

Now, after this update goes into effect, we can see these resources being served up by our service worker.

DevTools screenshot showing the resources loaded by the browser.  There are four jpeg images.

And we can see the corresponding cache that was created.

DevTools screenshot showing the new cache instance stored in Cache Storage.  Includes all cached images.

Add your own service worker content

Let’s say you want to apply with your service worker. Want to add some code to sync data with IndexedDB and add handlers to fetch and respond with IndexedDB data when the user is offline (again, my previous post walks through IndexedDB’s pan and entries). But how do you put your code into the service worker that Vite generated for us?

There is another Workbox option that we can use for this: importScripts.

VitePWA({
  workbox: {
    importScripts: ["sw-code.js"],

Here, the service worker will ask sw-code.js at run time. In this case, make sure that there is a file sw-code.js The file that your application can provide. The easiest way to achieve this is to put it in a file public folder (see Vite docs for detailed instructions).

If this file starts to grow to such a size that you need to separate things with JavaScript imports, be sure to compile it to prevent your service worker from trying to execute the import statements (which they may or may not be able to do). You can create a separate Vite build instead.

wrapping

At the end of 2021, CSS-Tricks asked a group of people on the front end what someone could do to improve their website. Chris Ferdinand suggested a service worker. Well, that’s exactly what we accomplished in this article and it was relatively simple, right? This is thanks to VitePWA with Hat Tips to Workbox and Cache API.

Service workers who make use of the Cache API can greatly improve the performance of a web application. And while it might seem a little intimidating or confusing at first, it’s good to know that we have tools like the VitePWA plugin to simplify things considerably. Install the plugin and let it do the heavy lifting. Sure, there are more advanced things a service worker can do, and VitePWA can be used for more complex functions, but an offline site is a great starting point!

Leave a Comment