用 Service Worker 实现前端性能优化
戳蓝字「前端技术优选」关注我们哦!
作者:RetroAstro
https://github.com/RetroAstro/cosmos-blog
前言
说起前端性能优化, 我们首先想到的可能就是用 Gulp 、Webpack 之类的自动化构建工具对 HTML、CSS 、JS 代码进行压缩,同时优化图片资源。再者就是使用 CSS Sprite 或者对于较小的图片用 base64 直接编码来进行优化。当然还有很多可以优化的方向,例如考虑浏览器缓存、页面渲染性能 ( 减少重排与重绘与 GPU 硬件加速 ) 、JS阻塞性能等等。但我们今天讲的是如何利用缓存策略在适宜的情况下直接减少对前端数据的请求量从而达到前端性能的优化。因此 Service Worker 以及其相关的 API 就成为了我们今天的主角。
提醒 : 本篇文章将直接讲述如何利用 Service Worker 对前端性能进行优化,希望读者在此之前已经对 Service Worker 有基本的了解,若之前没有接触过,可以先看看以下的两篇文章。
Service Worker ~ Google ( 墙 )
Service Worker 简介
制定缓存策略
首先,既然是前端性能优化,我们就需要想想该如何制定缓存策略才能达到理想的效果。我们可能有这样的想法,即对 CSS 、JS 等易更改文件优先使用网络请求的数据,而对于图片资源则优先使用缓存。如果再进一步思考的话,我们也许会希望在网络条件好的情况下优先使用网络请求数据,而网络条件较差时则尽可能的直接使用缓存。嗯 ~ 看起来还不错,那么根据以上的两点我们先用代码来实现一下吧。
先迈出最简单的第一步,注册 Service Worker。
// index.js
if ( 'serviceWorker' in navigator ) {
navigator.serviceWorker.register('/sw.js')
.then( registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch( err => console.log('ServiceWorker registration failed: ', err));
}
在 sw.js
中实现常规操作。
// sw.js
var cacheMaps = {
cache_file: 'css.js',
cache_image: 'images'
}
self.addEventListener('install', () => {
// 一般注册以后,激活需要等到再次刷新页面后再激活
// 可防止出现等待的情况,这意味着服务工作线程在安装完后立即激活
self.skipWaiting();
})
// 运行触发的事件
self.addEventListener('activate', event => {
event.waitUntil(
// 若缓存数据更改,则在这里更新缓存
caches.keys()
.then( cacheNames => {
return cacheNames.filter( item => !Object.values(cacheMaps).includes(item))
})
.then( keys => {
return Promise.all( keys.map( key => {
return caches.delete(key);
}))
})
// 更新客户端上的 Service Worker 脚本
.then(() => self.clients.claim())
)
})
实现网络优先的逻辑。
function firstNet(cacheName, request) {
// 请求网络数据并缓存
return fetch(request).then( response => {
var responseCopy = response.clone();
caches.open(cacheName).then( cache => {
cache.put(request, responseCopy);
});
return response;
}).catch(() => {
return caches.open(cacheName).then( cache => {
return cache.match(request);
});
});
}
实现缓存优先的逻辑。
function firstCache(cacheName, request) {
return caches.open(cacheName).then( cache => {
return cache.match(request).then( response => {
var fetchServer = function() {
return fetch(request).then( newResponse => {
cache.put(request, newResponse.clone());
return newResponse;
});
}
// 如果缓存中有数据则返回,否则请求网络数据
if (response) {
return response;
} else {
return fetchServer();
}
});
});
}
完成缓存策略中我们提到的第一点,即对 CSS 、JS 请求使用网络优先,图片资源请求实现缓存优先。
// sw.js
self.addEventListener('fetch', event => {
var
request = event.request,
url = request.url,
cacheName;
// 网络优先
if ( /\.(js|css)$/.test(url) ) {
(cacheName = cacheMaps.cache_file) && e.respondWith(firstNet(cacheName, request));
}
// 缓存优先
else if ( /\.(png|jpg|jpeg|gif|webp)$/.test(url) ) {
(cacheName = cacheMaps.cache_image) && e.respondWith(firstCache(cacheName, request));
}
})
接下来我们利用 Promise.race()
完成一个竞速模式,从而实现上文提到的第二点即根据网络条件的好坏执行相应的操作。
function networkCacheRace(cacheName, request) {
var timer, TIMEOUT = 500;
/**
* 网络好的情况下给网络请求500ms, 若超时则从缓存中取数据
* 若网络较差且没有缓存, 由于第一个 Promise 会一直处于 pending, 故此时等待网络请求响应
*/
return Promise.race([new Promise((resolve, reject) => {
timer = setTimeout(() => {
caches.open(cacheName).then( cache => {
cache.match(request).then( response => {
if (response) {
resolve(response);
}
});
});
}, TIMEOUT);
}), fetch(request).then( response => {
clearTimeout(timer);
var responseCopy = response.clone();
caches.open(cacheName).then( cache => {
cache.put(request, responseCopy);
});
return response;
}).catch(() => {
clearTimeout(timer);
return caches.open(cacheName).then( cache => {
return cache.match(request);
});
})]);
}
现在我们可以在 sw.js
中更改一下缓存策略,从而达到最理想的效果。
// sw.js
self.addEventListener('fetch', event => {
// ...
if ( /\.(js|css)$/.test(url) ) {
(cacheName = cacheMaps.cache_file)
&& e.respondWith(networkCacheRace(cacheName, request));
}
// ...
})
更好的方案 - Workbox
什么是 Workbox ? 我们可以看看谷歌开发者官网中给出的解释。
Workbox is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers.
其大概意思是它对常见的 Service Worker 操作进行了一层封装, 根据最佳实践方便了开发者的使用。因此在我们快速开发自己的 PWA 应用时使用 Workbox 是最合适不过的了。
它主要有以下几大功能 :
Precaching ~ 预缓存
Runtime caching ~ 运行时缓存
Strategies ~ 缓存策略
Request routing ~ 请求路由控制
Background sync ~ 后台同步
etc …
基于本文的内容, 在这里我们只谈谈如何简单的使用 Workbox 以及它所提供的几种缓存策略。
注意在 index.js
里面的注册操作不会改变, 变化的是 sw.js
中的代码。
// sw.js
// 导入谷歌提供的 Workbox 库
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.2.0/workbox-sw.js');
if ( !workbox ) {
console.log(`Workbox didn't load.`);
return;
}
// Workbox 注册成功, 可以进行下一步的操作
// 立即激活, 跳过等待
workbox.skipWaiting();
workbox.clientsClaim();
// workbox.routing.registerRoute()...
下面用官网给出的几张图解释一下 Workbox 所提供的几种缓存策略,而它们正好能满足上文我们自己用代码所实现的效果。
Stale-While-Revalidate
Cache First
Network First
Cache Only
Network Only
接下来让我们使用 Workbox 去实现上文优化前端性能的缓存策略。
缓存优先 :
workbox.routing.registerRoute(
/\.(png|jpg|jpeg|gif|webp)$/,
// 对于图片资源使用缓存优先
workbox.strategies.cacheFirst({
cacheName: 'images',
// 设置最大缓存数量以及过期时间
plugins: [
new workbox.expiration.Plugin({
maxEntries: 60,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
}),
);
网络优先 :
workbox.routing.registerRoute(
/\.(js|css)$/,
workbox.strategies.staleWhileRevalidate({
cacheName: 'css.js',
}),
);
由上文图中可看出 stale-while-revalidate 策略与我们实现的网络优先稍有不同,确切的来说更加明智,因为除了第一次需要网络请求,接下来的请求会直接从缓存中取数据但在页面加载之后会立即更新缓存,这样既保证了加载速度又能每次将数据准确的更新到最新版本。
竞速模式 :
workbox.routing.registerRoute(
/\.(js|css)$/,
workbox.strategies.networkFirst({
// 给网络请求0.5秒,若仍未返回则从缓存中取数据
networkTimetoutSeconds: 0.5,
cacheName: 'css.js',
}),
);
回头看看我们手动实现的缓存策略,显然使用 Workbox 要简单的多。当然 Workbox 中还有很多东西需要注意,但由于已经超出了文章所讲的主要内容因此在这里无法具体阐述,建议读者还是到官网去仔细看看文档详细了解一下,若因为墙的问题可以看看第二篇文章。
Workbox ~ Google ( 墙 )
神奇的 Workbox 3.0
后记
如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。鱼头的微信号是:krisChans95 也可以扫码关注公众号,订阅更多精彩内容。公众号窗口回复『 前端资料 』,即可获取约 200M 前端面试资料,不要错过。