现在很流行基于Github page和markdown的静态blog,非常适合技术的思维和习惯,针对不同的语言都有一些优秀的静态blog系统出现,如Jekyll/Ruby,Pelican/Python,Hexo/NodeJs,由于静态内容的特性非常适合做缓存来加速页面的访问,就利用Service worker来实现加速,结果是除了PageSpeed,CDN这些常见的服务器和网络加速之外,通过客户端实现了更好的访问体验。

加速/离线访问只需三步

  • 首页添加注册代码
1
2
3
4
5
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
  • 复制代码

https://alphayang.github.io/sw.js保存到你的网站根目录下

  • 修改不缓存域名列表及离线状态页面

在你的sw.js中修改

1
2
3
4
5
6
const ignoreFetch = [
/https?:\/\/cdn.bootcss.com\//,
/https?:\/\/static.duoshuo.com\//,
/https?:\/\/www.google-analytics.com\//,
/https?:\/\/dn-lbstatics.qbox.me\//,
];

打开Chrome Dev Tools->Source,看看自己的blog都引用了哪些第三方资源,逐个加到忽略列表里。

speedy-and-offline-site-by-service-worker/20170107093949130.png

在根目录下添加offline.html,在没有网络且缓存中也没有时使用,效果如下:

speedy-and-offline-site-by-service-worker/20170107100934328.png

在根目录下添加offline.svg,在无网络时图片资源请求返回该文件。

加速效果

首页加速后,网络请求从16降为1,加载时间从2.296s降为0.654s,得到了瞬间加载的结果。

speedy-and-offline-site-by-service-worker/20170107221445109.png

基于webpagetest

查看测试结果

加速/离线原理探索

什么是 Service worker

speedy-and-offline-site-by-service-worker/20170105133456161.png

如上图,Service worker 是一种由Javascript编写的浏览器端代理脚本,位于你的浏览器和服务器之间。当一个页面注册了一个 Service worker,它就可以注册一系列事件处理器来响应如网络请求和消息推送这些事件。Service worker 可以被用来管理缓存,当响应一个网络请求时可以配置为返回缓存还是从网络获取。由于Service worker 是基于事件的,所以它只在处理这些事件的时候被调入内存,不用担心常驻内存占用资源导致系统变慢。

Service worker生命周期

speedy-and-offline-site-by-service-worker/20170105135752765.png

Service worker 为网页添加一个类似于APP的生命周期,它只会响应系统事件,就算浏览器关闭时操作系统也可以唤醒Service worker,这点非常重要,让web app与native app的能力变得类似了。

Service worker在Register时会触发Install事件,在Install时可以用来预先获取和缓存应用所需的资源并设置每个文件的缓存策略。

一旦Service worker处于activated状态,就可以完全控制应用的资源,对网络请求进行检查,修改网络请求,从网络上获取并返回内容或是返回由已安装的Service worker预告获取并缓存好的资源,甚至还可以生成内容并返回给网络语法。

所有的这些都用户都是透明的,事实上,一个设计优秀的Service worker就像一个智能缓存系统,加强了网络和缓存功能,选择最优方式来响应网络请求,让应用更加稳定的运行,就算没有网络也没关系,因为你可以完全控制网络响应。

Service worker的控制从第二次页面访问开始

在首次加载页面时,所有资源都是从网络载的,Service worker 在首次加载时不会获取控制网络响应,它只会在后续访问页面时起作用。

speedy-and-offline-site-by-service-worker/20170105142817967.png

页面首次加载时完成install,并进入idle状态。

speedy-and-offline-site-by-service-worker/2017010514292700.png

页面第二次加载时,进入activated状态,准备处理所有的事件,同时 浏览器会向服务器发送一个异步 请求来检查Service worker本身是否有新的版本,构成了Service worker的更新机制。

speedy-and-offline-site-by-service-worker/20170105143604211.png

Service worker处理完所有的事件后,进入idle状态,最终进入terminated状态资源被释放,当有新的事件发生时再度被调用。

特点

  • 浏览器

Google Chrome,Firefox,Opera以及国内的各种双核浏览器都支持,但是 safari 不支持,那么在不支持的浏览器里Service worker不工作。

  • https

网站必须启用https来保证使用Service worker页面的安全性,开发时localhost默认认为是安全的。

  • non-block

Service worker 中的 Javascript 代码必须是非阻塞的,因为 localStorage 是阻塞性,所以不应该在 Service Worker 代码中使用 localStorage。

  • 单独的执行环境

Service worker运行在自己的全局环境中,通常也运行在自己单独的线程中。

  • 没有绑定到特定页面

service work能控制它所加载的整个范围内的资源。

  • 不能操作DOM

跟DOM所处的环境是相互隔离的。

speedy-and-offline-site-by-service-worker/20170107064257269.png

  • 没有浏览页面时也可以运行

接收系统事件,后台运行

  • 事件驱动,需要时运行,不需要时就终止

按需执行,只在需要时加载到内存

  • 可升级

执行时会异步获取最新的版本

实现加速/离线

Cache

网页缓存有很多,如HTTP缓存,localStorage,sessionStorage和cacheStorage都可以灵活搭配进行缓存,但操作太繁琐,直接使用更高级Service worker –本文的主人公。

添加Service worker入口

在web app的首页添加以下代码

1
2
3
4
5
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>

如果浏览器支持serviceWorker就注册它,不支持还是正常浏览,没有Service worker所提供的增强功能。

Service worker控制范围:
简单情况下,将sw.js放在网站的根目录下,这样Service worker可以控制网站所有的页面,,同理,如果把sw.js放在/my-app/sw.js那么它只能控制my-app目录下的页面。
sw.js放在/js/目录呢?更好的目录结构和范围控制呢?
在注册时指定js位置并设置范围。

1
2
3
4
5
6
7
navigator.serviceWorker.register('/js/sw.js', {scope: '/sw-test/'}).then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});

Service worker实现

监听三个事件:

1
2
3
4
5
self.addEventListener('install', onInstall);
self.addEventListener('fetch', onFetch);
self.addEventListener("activate", onActivate);

install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//////////
// Install
//////////
function onInstall(event) {
log('install event in progress.');
event.waitUntil(updateStaticCache());
}
function updateStaticCache() {
return caches
.open(cacheKey('offline'))
.then((cache) => {
return cache.addAll(offlineResources);
})
.then(() => {
log('installation complete!');
});
}

install时将所有符合缓存策略的资源进行缓存。

fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
////////
// Fetch
////////
function onFetch(event) {
const request = event.request;
if (shouldAlwaysFetch(request)) {
event.respondWith(networkedOrOffline(request));
return;
}
if (shouldFetchAndCache(request)) {
event.respondWith(networkedOrCached(request));
return;
}
event.respondWith(cachedOrNetworked(request));
}
onFetch做为浏览器网络请求的代理,根据需要返回网络或缓存内容,如果获取了网络内容,返回网络请求时同时进行缓存操作。

activate

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
///////////
// Activate
///////////
function onActivate(event) {
log('activate event in progress.');
event.waitUntil(removeOldCache());
}
function removeOldCache() {
return caches
.keys()
.then((keys) => {
return Promise.all( // We return a promise that settles when all outdated caches are deleted.
keys
.filter((key) => {
return !key.startsWith(version); // Filter by keys that don't start with the latest version prefix.
})
.map((key) => {
return caches.delete(key); // Return a promise that's fulfilled when each outdated cache is deleted.
})
);
})
.then(() => {
log('removeOldCache completed.');
});
}

在activate时根据version值来删除过期的缓存。

管理 Service worker

特定网站

  1. Google Chrome

Developer Tools->Application->Service Workers

speedy-and-offline-site-by-service-worker/20170107100514876.png

在这里还有三个非常有用的复选框:

  • Offline

模拟断网状态

  • Update on reload
    加载时更新

  • Bypass for network
    总是使用网络内容

  1. Firefox

只有在Settings里有一个可以在HTTP环境中使用Service worker的选项,适应于调试,没有单独网站下的Service worker管理。

speedy-and-offline-site-by-service-worker/20170107102700440.png

  1. Opera及其它双核浏览器同Google Chrome
    如果看到多个相同范围内的多个Service worker,说明Service woker更新后,而原有Service worker还没有被terminated。

浏览器全局

看看你的浏览器里都有哪些Service worker已经存在了

  1. Google Chrome

在地址栏里输入:

1
chrome://serviceworker-internals/

可以看到已经有24个Service worker了,在这里可以手动Start让它工作,也可以Unregister卸载掉。

speedy-and-offline-site-by-service-worker/20170106213323480.png

  1. Firefox

有两种方式进入Service worker管理界面来手动Start或unregister。

  • 菜单栏,Tool->Web Developer->Service workers

  • 地址栏中输入

1
about:debugging#workers
speedy-and-offline-site-by-service-worker/20170106213558243.png
  1. Opera及其它双核浏览器同Google Chrome

更多

TODO:

  • Service workers的更新需要手动编辑version,每次发布新文章时需要编辑。

  • 使用AMP让页面渲染速度达到最高。

Ref links

Service Worker Cookbook

Is service worker ready?

Chrome service worker status page

Firefox service worker status page

MS Edge service worker status page

WebKit service worker status page