用 Service Worker 實(shí)現(xiàn)前端性能優(yōu)化
戳藍(lán)字「前端技術(shù)優(yōu)選」關(guān)注我們哦!
作者:RetroAstro
https://github.com/RetroAstro/cosmos-blog
前言
說起前端性能優(yōu)化, 我們首先想到的可能就是用 Gulp 、Webpack 之類的自動(dòng)化構(gòu)建工具對 HTML、CSS 、JS 代碼進(jìn)行壓縮,同時(shí)優(yōu)化圖片資源。再者就是使用 CSS Sprite 或者對于較小的圖片用 base64 直接編碼來進(jìn)行優(yōu)化。當(dāng)然還有很多可以優(yōu)化的方向,例如考慮瀏覽器緩存、頁面渲染性能 ( 減少重排與重繪與 GPU 硬件加速 ) 、JS阻塞性能等等。但我們今天講的是如何利用緩存策略在適宜的情況下直接減少對前端數(shù)據(jù)的請求量從而達(dá)到前端性能的優(yōu)化。因此 Service Worker 以及其相關(guān)的 API 就成為了我們今天的主角。
提醒 : 本篇文章將直接講述如何利用 Service Worker 對前端性能進(jìn)行優(yōu)化,希望讀者在此之前已經(jīng)對 Service Worker 有基本的了解,若之前沒有接觸過,可以先看看以下的兩篇文章。
Service Worker ~ Google ( 墻 )
Service Worker 簡介
制定緩存策略
首先,既然是前端性能優(yōu)化,我們就需要想想該如何制定緩存策略才能達(dá)到理想的效果。我們可能有這樣的想法,即對 CSS 、JS 等易更改文件優(yōu)先使用網(wǎng)絡(luò)請求的數(shù)據(jù),而對于圖片資源則優(yōu)先使用緩存。如果再進(jìn)一步思考的話,我們也許會(huì)希望在網(wǎng)絡(luò)條件好的情況下優(yōu)先使用網(wǎng)絡(luò)請求數(shù)據(jù),而網(wǎng)絡(luò)條件較差時(shí)則盡可能的直接使用緩存。嗯 ~ 看起來還不錯(cuò),那么根據(jù)以上的兩點(diǎn)我們先用代碼來實(shí)現(xiàn)一下吧。
先邁出最簡單的第一步,注冊 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 中實(shí)現(xiàn)常規(guī)操作。
// sw.js
var cacheMaps = {
cache_file: 'css.js',
cache_image: 'images'
}
self.addEventListener('install', () => {
// 一般注冊以后,激活需要等到再次刷新頁面后再激活
// 可防止出現(xiàn)等待的情況,這意味著服務(wù)工作線程在安裝完后立即激活
self.skipWaiting();
})
// 運(yùn)行觸發(fā)的事件
self.addEventListener('activate', event => {
event.waitUntil(
// 若緩存數(shù)據(jù)更改,則在這里更新緩存
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())
)
})
實(shí)現(xiàn)網(wǎng)絡(luò)優(yōu)先的邏輯。
function firstNet(cacheName, request) {
// 請求網(wǎng)絡(luò)數(shù)據(jù)并緩存
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);
});
});
}
實(shí)現(xiàn)緩存優(yōu)先的邏輯。
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;
});
}
// 如果緩存中有數(shù)據(jù)則返回,否則請求網(wǎng)絡(luò)數(shù)據(jù)
if (response) {
return response;
} else {
return fetchServer();
}
});
});
}
完成緩存策略中我們提到的第一點(diǎn),即對 CSS 、JS 請求使用網(wǎng)絡(luò)優(yōu)先,圖片資源請求實(shí)現(xiàn)緩存優(yōu)先。
// sw.js
self.addEventListener('fetch', event => {
var
request = event.request,
url = request.url,
cacheName;
// 網(wǎng)絡(luò)優(yōu)先
if ( /\.(js|css)$/.test(url) ) {
(cacheName = cacheMaps.cache_file) && e.respondWith(firstNet(cacheName, request));
}
// 緩存優(yōu)先
else if ( /\.(png|jpg|jpeg|gif|webp)$/.test(url) ) {
(cacheName = cacheMaps.cache_image) && e.respondWith(firstCache(cacheName, request));
}
})
接下來我們利用 Promise.race() 完成一個(gè)競速模式,從而實(shí)現(xiàn)上文提到的第二點(diǎn)即根據(jù)網(wǎng)絡(luò)條件的好壞執(zhí)行相應(yīng)的操作。
function networkCacheRace(cacheName, request) {
var timer, TIMEOUT = 500;
/**
* 網(wǎng)絡(luò)好的情況下給網(wǎng)絡(luò)請求500ms, 若超時(shí)則從緩存中取數(shù)據(jù)
* 若網(wǎng)絡(luò)較差且沒有緩存, 由于第一個(gè) Promise 會(huì)一直處于 pending, 故此時(shí)等待網(wǎng)絡(luò)請求響應(yīng)
*/
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);
});
})]);
}
現(xiàn)在我們可以在 sw.js 中更改一下緩存策略,從而達(dá)到最理想的效果。
// sw.js
self.addEventListener('fetch', event => {
// ...
if ( /\.(js|css)$/.test(url) ) {
(cacheName = cacheMaps.cache_file)
&& e.respondWith(networkCacheRace(cacheName, request));
}
// ...
})
更好的方案 - Workbox
什么是 Workbox ? 我們可以看看谷歌開發(fā)者官網(wǎng)中給出的解釋。
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 操作進(jìn)行了一層封裝, 根據(jù)最佳實(shí)踐方便了開發(fā)者的使用。因此在我們快速開發(fā)自己的 PWA 應(yīng)用時(shí)使用 Workbox 是最合適不過的了。
它主要有以下幾大功能 :
Precaching ~ 預(yù)緩存
Runtime caching ~ 運(yùn)行時(shí)緩存
Strategies ~ 緩存策略
Request routing ~ 請求路由控制
Background sync ~ 后臺(tái)同步
etc …
基于本文的內(nèi)容, 在這里我們只談?wù)勅绾魏唵蔚氖褂?Workbox 以及它所提供的幾種緩存策略。
注意在 index.js 里面的注冊操作不會(huì)改變, 變化的是 sw.js 中的代碼。
// sw.js
// 導(dǎo)入谷歌提供的 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 注冊成功, 可以進(jìn)行下一步的操作
// 立即激活, 跳過等待
workbox.skipWaiting();
workbox.clientsClaim();
// workbox.routing.registerRoute()...
下面用官網(wǎng)給出的幾張圖解釋一下 Workbox 所提供的幾種緩存策略,而它們正好能滿足上文我們自己用代碼所實(shí)現(xiàn)的效果。
Stale-While-Revalidate

Cache First

Network First

Cache Only

Network Only

接下來讓我們使用 Workbox 去實(shí)現(xiàn)上文優(yōu)化前端性能的緩存策略。
緩存優(yōu)先 :
workbox.routing.registerRoute(
/\.(png|jpg|jpeg|gif|webp)$/,
// 對于圖片資源使用緩存優(yōu)先
workbox.strategies.cacheFirst({
cacheName: 'images',
// 設(shè)置最大緩存數(shù)量以及過期時(shí)間
plugins: [
new workbox.expiration.Plugin({
maxEntries: 60,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
}),
);
網(wǎng)絡(luò)優(yōu)先 :
workbox.routing.registerRoute(
/\.(js|css)$/,
workbox.strategies.staleWhileRevalidate({
cacheName: 'css.js',
}),
);
由上文圖中可看出 stale-while-revalidate 策略與我們實(shí)現(xiàn)的網(wǎng)絡(luò)優(yōu)先稍有不同,確切的來說更加明智,因?yàn)槌说谝淮涡枰W(wǎng)絡(luò)請求,接下來的請求會(huì)直接從緩存中取數(shù)據(jù)但在頁面加載之后會(huì)立即更新緩存,這樣既保證了加載速度又能每次將數(shù)據(jù)準(zhǔn)確的更新到最新版本。
競速模式 :
workbox.routing.registerRoute(
/\.(js|css)$/,
workbox.strategies.networkFirst({
// 給網(wǎng)絡(luò)請求0.5秒,若仍未返回則從緩存中取數(shù)據(jù)
networkTimetoutSeconds: 0.5,
cacheName: 'css.js',
}),
);
回頭看看我們手動(dòng)實(shí)現(xiàn)的緩存策略,顯然使用 Workbox 要簡單的多。當(dāng)然 Workbox 中還有很多東西需要注意,但由于已經(jīng)超出了文章所講的主要內(nèi)容因此在這里無法具體闡述,建議讀者還是到官網(wǎng)去仔細(xì)看看文檔詳細(xì)了解一下,若因?yàn)閴Φ膯栴}可以看看第二篇文章。
Workbox ~ Google ( 墻 )
神奇的 Workbox 3.0
后記
如果你喜歡探討技術(shù),或者對本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當(dāng)然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。魚頭的微信號(hào)是:krisChans95 也可以掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。公眾號(hào)窗口回復(fù)『 前端資料 』,即可獲取約 200M 前端面試資料,不要錯(cuò)過。
