Workbox

For optimizing front-end loading performance, many people use http-cache, asynchronous loading, 304 status codes, file compression, CDN and other methods to solve the problem. In fact, in addition to these methods, there is one more powerful than all of them, and that is Service Worker.

We can use Google Chrome team’s Workbox to implement Service Worker for rapid development.

Registering a Service Worker

Add the following to the page to register a Service Worker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
  // 检测是否支持 Service Worker
  // 也可使用 navigator.serviceWorker 判断
  if ('serviceWorker' in navigator) {
    // 为了保证首屏渲染性能,在页面 onload 完之后注册 Service Worker
    // 不使用 window.onload 以免冲突
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js');
    });
  }
</script>

Of course, you need to have a Service Worker code /sw.js before you can do that.

You can write the following code in this file to check if the Service Worker is successfully registered.

1
2
console.clear();
console.log('Successful registered service worker.');

service worker

1
2
3
importScripts(
  'https://storage.googleapis.com/workbox-cdn/releases/6.1.1/workbox-sw.js'
);

If you think Google’s CDN is not very reliable, you can use workbox-cli to store the resources locally.

1
2
npm i workbox-cli -g
workbox copyLibraries {path/to/workbox/}

In this case, you need to replace the content written above at the beginning of sw.js with the following

1
2
3
4
importScripts('{path/to}/workbox/workbox-sw.js');
workbox.setConfig({
  modulePathPrefix: '{path/to}/workbox/',
});

Workbox Policy

Stale While Revalidate

Stale While Revalidate

This policy will respond to network requests using a cache (if available) and update the cache in the background. If it is not cached, it will wait for a network response and use it. This is a fairly safe policy because it means that the user will update its cache periodically. The disadvantage of this policy is that it always requests resources from the network, which is more wasteful of the user’s bandwidth.

1
2
3
4
registerRoute(
  new RegExp(matchString),
  new workbox.strategies.StaleWhileRevalidate()
);

Network First

Network First

This policy will try to get a response from the network first. If a response is received, it will pass it to the browser and save it to the cache. If the network request fails, the last cached response will be used.

1
registerRoute(new RegExp(matchString), new workbox.strategies.NetworkFirst());

Cache First

Cache First

This policy will first check to see if there is a response in the cache and if so, use the policy. If the request is not in the cache, the network will be used and any valid response will be added to the cache before being passed to the browser.

1
registerRoute(new RegExp(matchString), new workbox.strategies.CacheFirst());

Network Only

Network Only

1
registerRoute(new RegExp(matchString), new workbox.strategies.NetworkOnly());

Forced to let the response come from the network.

Cache Only

Cache Only

Force the response to come from the cache.

1
registerRoute(new RegExp(matchString), new workbox.strategies.CacheOnly());

Policy Configuration

You can customize the behavior of the route by defining the plugins to be used.

1
2
3
4
5
6
7
8
9
new workbox.strategies.StaleWhileRevalidate({
    // Use a custom cache for this route.
    cacheName: 'my-cache-name',

    // Add an array of custom plugins (e.g. `ExpirationPlugin`).
    plugins: [
        ...
    ]
});

Custom Policies in Workbox

In some cases, you may want to use your own alternative policy to respond to requests, or just generate requests in the Service Worker via a template. To do this, you can provide a function handler that returns a Response object asynchronously.

1
2
3
4
5
const handler = async ({ url, event }) => {
  return new Response(`Custom handler response.`);
};

workbox.routing.registerRoute(new RegExp(matchString), handler);

Note that if a value is returned in a match callback, it passes handler as a params parameter to the callback.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const match = ({ url, event }) => {
  if (url.pathname === '/example') {
    return {
      name: 'Workbox',
      type: 'guide',
    };
  }
};

const handler = async ({ url, event, params }) => {
  // Response will be "A guide to Workbox"
  return new Response(`A ${params.type} to ${params.name}`);
};

workbox.routing.registerRoute(match, handler);

It may be useful for handler if some information in the URL can be parsed once in the match callback and used in it.

Workbox Practices

For most projects that use Workbox, you will usually introduce the appropriate gulp or webpack plugin, register the Service Worker in the build process, Precache the specified URL, generate sw.js, and so on. But for static site generators like Hexo, Jekyll, or CMSs like WordPress or Typecho, if you don’t install the corresponding plugins, you need to write a sw.js from scratch.

Let’s write the general configuration first.

1
2
3
4
5
6
7
let cacheSuffixVersion = '-210227'; // 缓存版本号
const maxEntries = 100; // 最大条目数

core.setCacheNameDetails({
  prefix: 'baoshuo-blog', // 前缀
  suffix: cacheSuffixVersion, // 后缀
});

Google Fonts

Google Fonts uses two main domains: fonts.googleapis.com and fonts.gstatic.com, so only caching is required when these two domains are matched.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
workbox.routing.registerRoute(
  // 匹配 fonts.googleapis.com 和 fonts.gstatic.com 两个域名
  new RegExp('^https://(?:fonts\\.googleapis\\.com|fonts\\.gstatic\\.com)'),
  new workbox.strategies.StaleWhileRevalidate({
    // cache storage 名称和版本号
    cacheName: 'font-cache' + cacheSuffixVersion,
    plugins: [
      // 使用 expiration 插件实现缓存条目数目和时间控制
      new workbox.expiration.ExpirationPlugin({
        // 最大保存项目
        maxEntries,
        // 缓存 30 天
        maxAgeSeconds: 30 * 24 * 60 * 60,
      }),
      // 使用 cacheableResponse 插件缓存状态码为 0 的请求
      new workbox.cacheableResponse.CacheableResponsePlugin({
        statuses: [0, 200],
      }),
    ],
  })
);

jsDelivr CDN

When using the jsDelivr CDN, if you specify the version of the library, the corresponding files can be said to be permanently unchanged, so use CacheFirst for caching.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
workbox.routing.registerRoute(
  new RegExp('^https://cdn\\.jsdelivr\\.net'),
  new workbox.strategies.CacheFirst({
    cacheName: 'static-immutable' + cacheSuffixVersion,
    fetchOptions: {
      mode: 'cors',
      credentials: 'omit',
    },
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxAgeSeconds: 30 * 24 * 60 * 60,
        purgeOnQuotaError: true,
      }),
    ],
  })
);

Google Analytics

Workbox has a Google Analytics Offline Statistics Plugin, but unfortunately I use the Sukka-written Unofficial Google Analytics Implementation, so I can only add a NetworkOnly to drop offline stats.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
workbox.routing.registerRoute(
  new RegExp('^https://api\\.baoshuo\\.ren/cfga/(.*)'),
  new workbox.strategies.NetworkOnly({
    plugins: [
      new workbox.backgroundSync.BackgroundSyncPlugin('Optical_Collect', {
        maxRetentionTime: 12 * 60, // Retry for max of 12 Hours (specified in minutes)
      }),
    ],
  })
);

Pictures

I am a LifeTime Premium VIP of SM.MS, so of course the pictures should be saved here~

MS has these image domains: i.loli.net, vip1.loli.net, vip2.loli.net, s1.baoshuo.ren, s1.baoshuo.ren, just write a regular match.

Since the files corresponding to the image links, like jsDelivr, are also almost never changed, use CacheFirst to cache them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
workbox.routing.registerRoute(
  new RegExp('^https://(?:i|vip[0-9])\\.loli\\.(?:io|net)'),
  new workbox.strategies.CacheFirst({
    cacheName: 'img-cache' + cacheSuffixVersion,
    plugins: [
      // 使用 expiration 插件实现缓存条目数目和时间控制
      new workbox.expiration.ExpirationPlugin({
        maxEntries, // 最大保存项目
        maxAgeSeconds: 30 * 24 * 60 * 60, // 缓存 30 天
      }),
      // 使用 cacheableResponse 插件缓存状态码为 0 的请求
      new workbox.cacheableResponse.CacheableResponsePlugin({
        statuses: [0, 200],
      }),
    ],
  })
);

These files are only updated occasionally, use StaleWhileRevalidate to balance speed and version updates.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
workbox.routing.registerRoute(
  new RegExp('^https://friends\\.baoshuo\\.ren(.*)(png|jpg|jpeg|svg|gif)'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'img-cache' + cacheSuffixVersion,
    fetchOptions: {
      mode: 'cors',
      credentials: 'omit',
    },
  })
);
workbox.routing.registerRoute(
  new RegExp('https://friends\\.baoshuo\\.ren/links.json'),
  new workbox.strategies.StaleWhileRevalidate()
);

Disqus Comments

DisqusJS determines the availability of Disqus for visitors by checking shortname.disqus.com/favicon. and disqus.com/favicon., which obviously cannot be cached. The API can use NetworkFirst when there is no network to achieve the effect of viewing comments even when there is no network. Also, Disqus itself does not need to cache, so using NetworkOnly for *.disqus.com is fine. However, the avatars, JS and CSS under *.disquscdn.com can be cached for some time, so use CacheFirst to cache them for 10 days.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// API
workbox.routing.registerRoute(
  new RegExp('^https://api\\.baoshuo\\.ren/disqus/(.*)'),
  new workbox.strategies.NetworkFirst({
    cacheName: 'dsqjs-api' + cacheSuffixVersion,
    fetchOptions: {
      mode: 'cors',
      credentials: 'omit',
    },
    networkTimeoutSeconds: 3,
  })
);
// Disqus
workbox.routing.registerRoute(
  new RegExp('^https://(.*)disqus\\.com'),
  new workbox.strategies.NetworkOnly()
);
workbox.routing.registerRoute(
  new RegExp('^https://(.*)disquscdn\\.com(.*)'),
  new workbox.strategies.CacheFirst({
    cacheName: 'disqus-cdn-cache' + cacheSuffixVersion,
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxAgeSeconds: 10 * 24 * 60 * 60,
      }),
      new workbox.cacheableResponse.CacheableResponsePlugin({
        statuses: [0, 200],
      }),
    ],
  })
);

Suffix Matching

For the rest of the static files that are not matched by the domain name, match by file suffix and use StaleWhileRevalidate to balance speed and version updates.

1
2
3
4
5
6
7
8
workbox.routing.registerRoute(
  new RegExp('.*.(?:png|jpg|jpeg|svg|gif|webp)'),
  new workbox.strategies.StaleWhileRevalidate()
);
workbox.routing.registerRoute(
  new RegExp('.*.(css|js)'),
  new workbox.strategies.StaleWhileRevalidate()
);

Default behavior

Use Workbox’s defaultHandler to match the rest of the requests (including the page itself), always using NetworkFirst to speed up and take offline with Workbox’s runtimeCache.

1
2
3
4
5
workbox.routing.setDefaultHandler(
  new workbox.strategies.NetworkFirst({
    networkTimeoutSeconds: 3,
  })
);

Article cover image from: https://developers.google.com/web/tools/workbox

The image in the Workbox Strategies section is from: https://web.dev/offline-cookbook/