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.');
|
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
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
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
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
1
|
registerRoute(new RegExp(matchString), new workbox.strategies.NetworkOnly());
|
Forced to let the response come from the network.
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],
}),
],
})
);
|
Links
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()
);
|
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/